Skip to content

Commit 3840134

Browse files
committed
feat: polkadot
1 parent 81ac35d commit 3840134

File tree

8 files changed

+300
-5
lines changed

8 files changed

+300
-5
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/sign-client/src/constants/engine.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,9 @@ export const TVF_METHODS = {
223223
stacks_stxTransfer: {
224224
key: "txId",
225225
},
226+
227+
// polkadot
228+
polkadot_signTransaction: {
229+
key: "",
230+
},
226231
};

packages/sign-client/src/controllers/engine.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import {
103103
mergeRequiredAndOptionalNamespaces,
104104
getNearTransactionIdFromSignedTransaction,
105105
getAlgorandTransactionId,
106+
buildSignedExtrinsicHash,
106107
} from "@walletconnect/utils";
107108
import EventEmmiter from "events";
108109
import {
@@ -3114,14 +3115,13 @@ export class Engine extends IEngine {
31143115
params: JsonRpcTypes.RequestParams["wc_sessionRequest"],
31153116
result?: any,
31163117
) => {
3117-
const requestMethod = params.request.method;
31183118
const tvf: RelayerTypes.ITVF = {
31193119
correlationId: id,
3120-
rpcMethods: [requestMethod],
3120+
rpcMethods: [params.request.method],
31213121
chainId: params.chainId,
31223122
};
31233123
try {
3124-
const txHashes = this.extractTxHashesFromResult(requestMethod, result);
3124+
const txHashes = this.extractTxHashesFromResult(params.request, result);
31253125
tvf.txHashes = txHashes;
31263126
tvf.contractAddresses = this.isValidContractData(params.request.params)
31273127
? [params.request.params?.[0]?.to]
@@ -3147,8 +3147,14 @@ export class Engine extends IEngine {
31473147
return false;
31483148
};
31493149

3150-
private extractTxHashesFromResult = (method: string, result: any): string[] => {
3150+
private extractTxHashesFromResult = (
3151+
request: JsonRpcTypes.RequestParams["wc_sessionRequest"]["request"],
3152+
result: any,
3153+
): string[] => {
31513154
try {
3155+
if (!result) return [];
3156+
3157+
const method = request.method;
31523158
const methodConfig = TVF_METHODS[method as keyof typeof TVF_METHODS];
31533159

31543160
if (method === "sui_signTransaction") {
@@ -3171,6 +3177,15 @@ export class Engine extends IEngine {
31713177
return [result.tx_json?.hash];
31723178
}
31733179

3180+
if (method === "polkadot_signTransaction") {
3181+
return [
3182+
buildSignedExtrinsicHash({
3183+
transaction: request.params.transactionPayload,
3184+
signature: result.signature,
3185+
}),
3186+
];
3187+
}
3188+
31743189
if (method === "algo_signTxn") {
31753190
return isValidArray(result)
31763191
? result.map((tx: any) => getAlgorandTransactionId(tx))

packages/sign-client/test/sdk/client.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -687,6 +687,11 @@ describe("Sign Client Integration", () => {
687687
events: [],
688688
chains: ["stacks:mainnet"],
689689
},
690+
polkadot: {
691+
methods: ["polkadot_signTransaction"],
692+
events: [],
693+
chains: ["polkadot:mainnet"],
694+
},
690695
},
691696
namespaces: {
692697
solana: {
@@ -756,6 +761,12 @@ describe("Sign Client Integration", () => {
756761
chains: ["stacks:mainnet"],
757762
accounts: ["stacks:mainnet:0x"],
758763
},
764+
polkadot: {
765+
methods: ["polkadot_signTransaction"],
766+
events: [],
767+
chains: ["polkadot:mainnet"],
768+
accounts: ["polkadot:mainnet:0x"],
769+
},
759770
},
760771
},
761772
);
@@ -2198,6 +2209,108 @@ describe("Sign Client Integration", () => {
21982209
}),
21992210
]);
22002211

2212+
// polkadot polkadot_signTransaction example
2213+
await Promise.all([
2214+
new Promise<void>((resolve) => {
2215+
clients.B.once("session_request", async (args) => {
2216+
const pendingRequests = clients.B.pendingRequest.getAll();
2217+
const { id, topic, params } = pendingRequests[0];
2218+
expect(params).toEqual(args.params);
2219+
expect(topic).toEqual(args.topic);
2220+
expect(id).toEqual(args.id);
2221+
const expectedTxHashes = [
2222+
"0x48016b3c80b7b61d32d1db6f52038de70d7d30ef948da047442cc9c952b92e84",
2223+
];
2224+
const transaction = {
2225+
signature:
2226+
"362cef5dff66aee851a5d8c5100a53590eddd7c75c1a53553b08861fb28ce80b96d53279f52a27c866639954c5efa32b52c148fefe78dbdad1f9d3be4f44538f",
2227+
};
2228+
2229+
const result = formatJsonRpcResult(id, transaction);
2230+
2231+
let checkedWalletPublish = false;
2232+
clients.B.core.relayer.once(RELAYER_EVENTS.publish, (publishPayload: any) => {
2233+
const tvf = publishPayload.tvf;
2234+
if (!tvf) {
2235+
return console.error("polkadot tvf is undefined");
2236+
}
2237+
if (!tvf.chainId || !tvf.rpcMethods || !tvf.txHashes) {
2238+
return console.error("polkadot tvf is missing required fields");
2239+
}
2240+
if (
2241+
tvf.rpcMethods.length !== 1 &&
2242+
tvf.rpcMethods[0] !== "polkadot_signTransaction"
2243+
) {
2244+
return console.error("polkadot tvf rpcMethods is invalid", tvf.rpcMethods);
2245+
}
2246+
if (tvf.txHashes.join(",") !== expectedTxHashes.join(",")) {
2247+
return console.error(
2248+
"polkadot txHashes do not match: transactionId",
2249+
tvf.txHashes,
2250+
expectedTxHashes,
2251+
);
2252+
}
2253+
2254+
checkedWalletPublish = true;
2255+
});
2256+
2257+
await clients.B.respond({
2258+
topic,
2259+
response: result,
2260+
});
2261+
2262+
expect(checkedWalletPublish).to.be.true;
2263+
resolve();
2264+
});
2265+
}),
2266+
new Promise<void>(async (resolve) => {
2267+
const requestParams = {
2268+
method: "polkadot_signTransaction",
2269+
params: {
2270+
address: "15JBFhDp1rQycRFuCtkr2VouMiWyDzh3qRUPA8STY53mdRmM",
2271+
transactionPayload: {
2272+
method:
2273+
"050300c07d211d3c181df768d9d9d41df6f14f9d116d9c1906f38153b208259c315b4b02286bee",
2274+
specVersion: "c9550f00",
2275+
transactionVersion: "1a000000",
2276+
genesisHash: "91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3",
2277+
blockHash: "130d8c27af0e0adfa85d370af89746f229780b677c81da97d11a4921cdb86df5",
2278+
era: "1502",
2279+
nonce: "1c",
2280+
tip: "00",
2281+
mode: "00",
2282+
metadataHash: "00",
2283+
address: "15JBFhDp1rQycRFuCtkr2VouMiWyDzh3qRUPA8STY53mdRmM",
2284+
version: 4,
2285+
},
2286+
},
2287+
};
2288+
let checkedDappPublish = false;
2289+
2290+
clients.A.core.relayer.once(RELAYER_EVENTS.publish, (publishPayload: any) => {
2291+
checkedDappPublish = true;
2292+
const tvf = publishPayload.tvf;
2293+
expect(tvf).to.exist;
2294+
expect(tvf?.chainId).to.eq(TEST_REQUEST_PARAMS.chainId);
2295+
expect(tvf?.rpcMethods).to.eql([requestParams.method]);
2296+
expect(tvf?.txHashes).to.be.undefined;
2297+
expect(tvf?.contractAddresses).to.eql([requestParams.params[0].to]);
2298+
});
2299+
2300+
await clients.A.request({
2301+
topic,
2302+
...TEST_REQUEST_PARAMS,
2303+
request: {
2304+
...TEST_REQUEST_PARAMS.request,
2305+
...requestParams,
2306+
},
2307+
chainId: "polkadot:mainnet",
2308+
});
2309+
expect(checkedDappPublish).to.be.true;
2310+
resolve();
2311+
}),
2312+
]);
2313+
22012314
await throttle(1_000);
22022315
await deleteClients(clients);
22032316
});

packages/utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"prettier": "prettier --check '{src,test}/**/*.{js,ts,jsx,tsx}'"
3232
},
3333
"dependencies": {
34+
"blakejs": "1.2.1",
3435
"@scure/base": "1.2.6",
3536
"@msgpack/msgpack": "3.1.2",
3637
"@noble/ciphers": "1.3.0",

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export * from "./namespaces";
1010
export * from "./network";
1111
export * from "./memoryStore";
1212
export * from "./signatures";
13+
export * from "./polkadot";

packages/utils/src/polkadot.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import bs58 from "bs58";
2+
import { blake2b } from "blakejs";
3+
4+
export function ss58AddressToPublicKey(address: string): Uint8Array {
5+
const decoded = bs58.decode(address);
6+
if (decoded.length < 33) throw new Error("Too short to contain a public key");
7+
return decoded.slice(1, 33);
8+
}
9+
10+
export function addSignatureToExtrinsic({
11+
publicKey,
12+
signature,
13+
payload,
14+
}: {
15+
publicKey: Uint8Array;
16+
signature: Uint8Array;
17+
payload: any;
18+
}): Uint8Array {
19+
const method = hexToBytes(payload.method);
20+
const version = parseInt(payload.version?.toString() || "4");
21+
const extrinsicVersion = 0x80 | version;
22+
23+
const signatureType = guessSignatureTypeFromAddress(payload.address);
24+
25+
const era = payload.era === "00" ? new Uint8Array([0x00]) : hexToBytes(payload.era);
26+
if (era.length !== 1 && era.length !== 2) throw new Error("Invalid era length");
27+
28+
const nonce = parseInt(payload.nonce, 16);
29+
const nonceBytes = new Uint8Array([nonce & 0xff, (nonce >> 8) & 0xff]);
30+
31+
const tip = BigInt(`0x${normalizeHex(payload.tip)}`);
32+
const tipBytes = compactEncodeBigInt(tip);
33+
34+
const body = new Uint8Array([
35+
0x00, // MultiAddress::Id
36+
...publicKey,
37+
signatureType,
38+
...signature,
39+
...era,
40+
...nonceBytes,
41+
...tipBytes,
42+
...method,
43+
]);
44+
45+
const lengthPrefix = compactEncodeInt(body.length + 1);
46+
return new Uint8Array([...lengthPrefix, extrinsicVersion, ...body]);
47+
}
48+
49+
export function deriveExtrinsicHash(signedExtrinsicHex: string): string {
50+
const bytes = hexToBytes(signedExtrinsicHex);
51+
const hash = blake2b(bytes, undefined, 32);
52+
return "0x" + Buffer.from(hash).toString("hex");
53+
}
54+
55+
function hexToBytes(hex: string): Uint8Array {
56+
return new Uint8Array(
57+
hex
58+
.replace(/^0x/, "")
59+
.match(/.{1,2}/g)!
60+
.map((byte) => parseInt(byte, 16)),
61+
);
62+
}
63+
64+
function normalizeHex(input: string): string {
65+
return input.startsWith("0x") ? input.slice(2) : input;
66+
}
67+
68+
function guessSignatureTypeFromAddress(address: string): number {
69+
const decoded = bs58.decode(address);
70+
const prefix = decoded[0];
71+
if (prefix === 42) return 0x00; // Ed25519
72+
if (prefix === 60) return 0x02; // Secp256k1
73+
return 0x01; // Default Sr25519
74+
}
75+
76+
function compactEncodeInt(value: number): Uint8Array {
77+
if (value < 1 << 6) {
78+
return new Uint8Array([value << 2]);
79+
} else if (value < 1 << 14) {
80+
const val = (value << 2) | 0x01;
81+
return new Uint8Array([val & 0xff, (val >> 8) & 0xff]);
82+
} else if (value < 1 << 30) {
83+
const val = (value << 2) | 0x02;
84+
return new Uint8Array([val & 0xff, (val >> 8) & 0xff, (val >> 16) & 0xff, (val >> 24) & 0xff]);
85+
} else {
86+
throw new Error("Compact encoding > 2^30 not supported");
87+
}
88+
}
89+
90+
function compactEncodeBigInt(value: bigint): Uint8Array {
91+
if (value < 1n << 6n) {
92+
return new Uint8Array([Number(value << 2n)]);
93+
} else if (value < 1n << 14n) {
94+
const val = (value << 2n) | 0x01n;
95+
return new Uint8Array([Number(val & 0xffn), Number((val >> 8n) & 0xffn)]);
96+
} else if (value < 1n << 30n) {
97+
const val = (value << 2n) | 0x02n;
98+
return new Uint8Array([
99+
Number(val & 0xffn),
100+
Number((val >> 8n) & 0xffn),
101+
Number((val >> 16n) & 0xffn),
102+
Number((val >> 24n) & 0xffn),
103+
]);
104+
} else {
105+
throw new Error("BigInt compact encoding not supported > 2^30");
106+
}
107+
}
108+
109+
export function buildSignedExtrinsicHash(payload: {
110+
transaction: {
111+
method: string;
112+
era: string;
113+
nonce: string;
114+
tip: string;
115+
mode: string;
116+
address: string;
117+
version: number;
118+
};
119+
signature: string;
120+
}) {
121+
const signature = Uint8Array.from(Buffer.from(payload.signature, "hex"));
122+
123+
const publicKey = ss58AddressToPublicKey(payload.transaction.address);
124+
const signed = addSignatureToExtrinsic({ publicKey, signature, payload: payload.transaction });
125+
const hexSigned = Buffer.from(signed).toString("hex");
126+
const hash = deriveExtrinsicHash(hexSigned);
127+
128+
return hash;
129+
}

0 commit comments

Comments
 (0)