Skip to content

Commit

Permalink
Merge pull request #261 from oasisprotocol/CedarMist/ethersv6-fixes
Browse files Browse the repository at this point in the history
Fixes some minor issues to make it work with Ethers v5 & v6,
  • Loading branch information
CedarMist authored Jan 24, 2024
2 parents 9b899bb + be038b2 commit 984e7c4
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 42 deletions.
1 change: 1 addition & 0 deletions clients/js/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ lib/
node_modules/
package-lock.json
*.log
oasisprotocol-sapphire-paratime-*.tgz
16 changes: 10 additions & 6 deletions clients/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"type": "module",
"name": "@oasisprotocol/sapphire-paratime",
"license": "Apache-2.0",
"version": "1.2.2",
"version": "1.3.0",
"description": "The Sapphire ParaTime Web3 integration library.",
"homepage": "https://github.com/oasisprotocol/sapphire-paratime/tree/main/clients/js",
"repository": {
Expand Down Expand Up @@ -42,11 +42,11 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@oasisprotocol/deoxysii": "^0.0.5",
"cborg": "^1.9.5",
"ethers": "^6.6.1",
"js-sha512": "^0.8.0",
"tweetnacl": "^1.0.3",
"@oasisprotocol/deoxysii": "0.0.5",
"cborg": "^1.10.2",
"ethers": "^6.10.0",
"@noble/hashes": "1.3.2",
"tweetnacl": "1.0.3",
"type-fest": "^2.19.0"
},
"devDependencies": {
Expand All @@ -65,5 +65,9 @@
"ts-node": "^10.9.2",
"typedoc": "^0.25.4",
"typescript": "^4.8.3"
},
"peerDependencies": {
"ethers": "6.x",
"@noble/hashes": "1.x"
}
}
14 changes: 10 additions & 4 deletions clients/js/src/cipher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as cbor from 'cborg';
import { BytesLike, isBytesLike, hexlify, getBytes } from 'ethers';
import deoxysii from '@oasisprotocol/deoxysii';
import { sha512_256 } from 'js-sha512';
import { sha512_256 } from '@noble/hashes/sha512';
import { hmac } from '@noble/hashes/hmac';
import nacl, { BoxKeyPair } from 'tweetnacl';
import { Promisable } from 'type-fest';

Expand Down Expand Up @@ -193,10 +194,15 @@ export class X25519DeoxysII extends Cipher {
super();
this.publicKey = keypair.publicKey;
// Derive a shared secret using X25519 (followed by hashing to remove ECDH bias).
const keyBytes = sha512_256.hmac
.create('MRAE_Box_Deoxys-II-256-128')

const keyBytes = hmac
.create(
sha512_256,
new TextEncoder().encode('MRAE_Box_Deoxys-II-256-128'),
)
.update(nacl.scalarMult(keypair.secretKey, peerPublicKey))
.arrayBuffer();
.digest().buffer;

this.key = new Uint8Array(keyBytes);
this.cipher = new deoxysii.AEAD(new Uint8Array(this.key)); // deoxysii owns the input
}
Expand Down
66 changes: 49 additions & 17 deletions clients/js/src/compat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,11 @@ function isEthers5Signer(upstream: object): upstream is Ethers5Signer {
}

function isEthers6Signer(upstream: object): upstream is Signer {
// XXX: this will not match if installed ethers version is different!
return upstream instanceof AbstractSigner;
return (
upstream instanceof AbstractSigner ||
(Reflect.get(upstream, 'signTypedData') &&
Reflect.get(upstream, 'signTransaction'))
);
}

function isEthersSigner(upstream: object): upstream is Signer | Ethers5Signer {
Expand All @@ -305,8 +308,13 @@ function isEthers5Provider(upstream: object): upstream is Ethers5Signer {
}

function isEthers6Provider(upstream: object): upstream is Provider {
// XXX: this will not match if installed ethers version is different!
return upstream instanceof AbstractProvider;
//
return (
upstream instanceof AbstractProvider ||
(Reflect.get(upstream, 'waitForBlock') &&
Reflect.get(upstream, 'destroy') &&
Reflect.get(upstream, 'broadcastTransaction'))
);
}

function isEthersProvider(
Expand Down Expand Up @@ -590,61 +598,85 @@ export async function fetchRuntimePublicKeyByChainId(
chainId: number,
opts?: { fetch?: typeof fetch },
): Promise<Uint8Array> {
const { defaultGateway: gatewayUrl } = NETWORKS[chainId];
if (!gatewayUrl)
const { defaultGateway } = NETWORKS[chainId];
if (!defaultGateway)
throw new Error(
`Unable to fetch runtime public key for network with unknown ID: ${chainId}.`,
);
const fetchImpl = opts?.fetch ?? globalThis?.fetch;
const res = await (fetchImpl
? fetchRuntimePublicKeyBrowser(gatewayUrl, fetchImpl)
: fetchRuntimePublicKeyNode(gatewayUrl));
? fetchRuntimePublicKeyBrowser(defaultGateway, fetchImpl)
: fetchRuntimePublicKeyNode(defaultGateway));
return getBytes(res.result.key);
}

function fromQuantity(x: number | string): number {
if (typeof x == 'string') {
if (x.startsWith('0x')) {
return parseInt(x, 16);
}
return parseInt(x); // Assumed to be base 10
}
return x;
}

/**
* Picks the most user-trusted runtime calldata public key source based on what
* connections are available.
*
* NOTE: MetaMask does not support Web3 methods it doesn't know about, so we have to
* fall-back to manually querying the default gateway.
*/
export async function fetchRuntimePublicKey(
upstream: UpstreamProvider,
): Promise<Uint8Array> {
const provider = 'provider' in upstream ? upstream['provider'] : upstream;
let chainId: number | undefined;
if (provider) {
let resp: any;
// It's probably an EIP-1193 provider
if ('request' in provider) {
const source = provider as EIP1193Provider;
try {
const source = provider as EIP1193Provider;
resp = await source.request({
method: OASIS_CALL_DATA_PUBLIC_KEY,
params: [],
});
} catch (ex) {
// don't do anything, move on to try next
chainId = fromQuantity(
(await source.request({ method: 'eth_chainId' })) as string | number,
);
}
}
// If it's a `send` provider
else if ('send' in provider) {
const source = provider as {
send: (method: string, params: any[]) => Promise<any>;
};
try {
const source = provider as {
send: (method: string, params: any[]) => Promise<any>;
};
resp = await source.send(OASIS_CALL_DATA_PUBLIC_KEY, []);
} catch (ex) {
// don't do anything, move on to try chainId fetch
chainId = fromQuantity(await source.send('eth_chainId', []));
}
}
// Otherwise, we have no idea what to do with this provider!
else {
throw new Error(
'fetchRuntimePublicKey does not support non-request non-send provier!',
);
}
if (resp && 'key' in resp) {
const key = resp.key;
return getBytes(key);
}
}

// Note: MetaMask does not support Web3 methods it doesn't know about, so we have to
// fall back to manually querying the default gateway.
const chainId = Number(
(await new BrowserProvider(provider).getNetwork()).chainId,
);
if (!chainId) {
throw new Error(
'fetchRuntimePublicKey failed to retrieve chainId from provider',
);
}
return fetchRuntimePublicKeyByChainId(chainId);
}
44 changes: 29 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 984e7c4

Please sign in to comment.