Skip to content

Commit 6adf7e8

Browse files
feat: added wallet-v5r1 integration (#17)
1 parent 7e22bcc commit 6adf7e8

2 files changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import type * as nt from 'nekoton-wasm';
2+
import { Address } from 'everscale-inpage-provider';
3+
import { BigNumber } from 'bignumber.js';
4+
5+
import core from '../../core';
6+
import { Account, PrepareMessageParams, AccountsStorageContext } from './';
7+
8+
/**
9+
* @category AccountsStorage
10+
*/
11+
export class WalletV5R1Account implements Account {
12+
public readonly address: Address;
13+
private publicKey?: BigNumber;
14+
private nonce?: number;
15+
private isDeployed?: boolean;
16+
17+
public static async computeAddress(args: {
18+
publicKey: string | BigNumber;
19+
workchain?: number;
20+
nonce?: number;
21+
}): Promise<Address> {
22+
await core.ensureNekotonLoaded();
23+
24+
const publicKey = args.publicKey instanceof BigNumber ? args.publicKey : new BigNumber(`0x${args.publicKey}`);
25+
const hash = makeStateInit(publicKey, args.nonce).hash;
26+
27+
return new Address(`${args.workchain != null ? args.workchain : 0}:${hash}`);
28+
}
29+
30+
public static async fromPubkey(args: {
31+
publicKey: string;
32+
workchain?: number;
33+
nonce?: number;
34+
}): Promise<WalletV5R1Account> {
35+
const publicKey = new BigNumber(`0x${args.publicKey}`);
36+
const address = await WalletV5R1Account.computeAddress({ publicKey, workchain: args.workchain, nonce: args.nonce });
37+
38+
const result = new WalletV5R1Account(address);
39+
result.publicKey = publicKey;
40+
result.nonce = args.nonce;
41+
42+
return result;
43+
}
44+
45+
constructor(address: Address) {
46+
this.address = address;
47+
}
48+
49+
async fetchPublicKey(ctx: AccountsStorageContext): Promise<string> {
50+
let publicKey = this.publicKey;
51+
52+
if (publicKey == null) {
53+
publicKey = this.publicKey = await this.fetchState(ctx).then(s => new BigNumber(s.publicKey));
54+
}
55+
56+
return publicKey.toString(16).padStart(64, '0');
57+
}
58+
59+
async prepareMessage(args: PrepareMessageParams, ctx: AccountsStorageContext): Promise<nt.SignedMessage> {
60+
const { publicKey, seqno, stateInit } = await this.fetchState(ctx);
61+
62+
const signer = await ctx.getSigner(publicKey);
63+
64+
const body = args.payload ? ctx.encodeInternalInput(args.payload) : '';
65+
const validUntil = ctx.nowSec + args.timeout;
66+
67+
const actions = ctx.packIntoCell({
68+
structure: ACTIONS,
69+
abiVersion: '2.3',
70+
data: {
71+
prefix: PREFIX.SEND_MESSAGE,
72+
mode: 3,
73+
nextAction: '',
74+
currAction: ctx.encodeInternalMessage({
75+
dst: args.recipient,
76+
bounce: args.bounce,
77+
stateInit: args.stateInit,
78+
body,
79+
amount: args.amount,
80+
}),
81+
},
82+
}).boc;
83+
84+
const params: nt.TokensObject = {
85+
op: PREFIX.SIGNED_EXTERNAL,
86+
walletId: this.nonce || 0,
87+
validUntil,
88+
seqno,
89+
actions,
90+
extended: null,
91+
};
92+
93+
const signature = core.nekoton.extendSignature(
94+
await signer.sign(core.nekoton.packIntoCell(DATA_TO_SIGN, params, '2.3').hash, args.signatureContext),
95+
);
96+
97+
const data: nt.TokensObject = {
98+
walletId: this.nonce || 0,
99+
validUntil,
100+
seqno,
101+
actions,
102+
extended: null,
103+
signatureHigh: signature.signatureParts.high,
104+
signatureLow: signature.signatureParts.low,
105+
};
106+
107+
return ctx.createRawExternalMessage({
108+
address: this.address,
109+
expireAt: validUntil,
110+
body: ctx.encodeInternalInput({ abi: WALLET_V5R1_ABI, method: 'sendTransaction', params: data }),
111+
stateInit,
112+
});
113+
}
114+
115+
private async fetchState(
116+
ctx: AccountsStorageContext,
117+
): Promise<{ publicKey: string; seqno: number; stateInit?: string }> {
118+
// seqno changes after each message
119+
const state = await ctx.getFullContractState(this.address);
120+
121+
this.isDeployed = !!state?.isDeployed;
122+
let seqno = 0;
123+
let stateInit;
124+
125+
if (!!state && this.isDeployed) {
126+
const data = core.nekoton.extractContractData(state.boc);
127+
128+
if (!data) {
129+
throw new Error('Contract is deployed but its data is missing');
130+
}
131+
132+
const parsedData = core.nekoton.unpackFromCell(DATA_STRUCTURE, data, false, '2.3');
133+
134+
this.publicKey = new BigNumber(parsedData.publicKey as string);
135+
seqno = parsedData.seqno as number;
136+
} else {
137+
stateInit = makeStateInit(this.publicKey!, this.nonce).boc;
138+
}
139+
140+
return {
141+
publicKey: this.publicKey!.toString(16).padStart(64, '0'),
142+
stateInit,
143+
seqno,
144+
};
145+
}
146+
}
147+
148+
const makeStateInit = (publicKey: BigNumber, nonce?: number): { boc: string; hash: string } => {
149+
const tokens: nt.TokensObject = {
150+
isSignatureAllowed: true,
151+
seqno: 0,
152+
walletId: nonce || 0,
153+
publicKey: publicKey.toFixed(0),
154+
extensions: null,
155+
};
156+
157+
const data = core.nekoton.packIntoCell(DATA_STRUCTURE, tokens).boc;
158+
159+
return core.nekoton.mergeTvc(WALLET_V5R1_CODE, data);
160+
};
161+
162+
const DATA_STRUCTURE: nt.AbiParam[] = [
163+
{ name: 'isSignatureAllowed', type: 'bool' }, // allow signed external/internal messages
164+
{ name: 'seqno', type: 'uint32' }, // internal counter. starts from 0
165+
{ name: 'walletId', type: 'uint32' }, // nonce to deploy few wallets for the same public key
166+
{ name: 'publicKey', type: 'uint256' },
167+
{ name: 'extensions', type: 'optional(cell)' },
168+
];
169+
170+
const DATA_TO_SIGN: nt.AbiParam[] = [
171+
{ name: 'op', type: 'uint32' },
172+
{ name: 'walletId', type: 'uint32' },
173+
{ name: 'validUntil', type: 'uint32' },
174+
{ name: 'seqno', type: 'uint32' },
175+
{ name: 'actions', type: 'optional(cell)' },
176+
{ name: 'extended', type: 'optional(cell)' },
177+
];
178+
179+
const ACTIONS: nt.AbiParam[] = [
180+
{ name: 'prefix', type: 'uint32' },
181+
{ name: 'mode', type: 'uint8' },
182+
{ name: 'nextAction', type: 'cell' },
183+
{ name: 'currAction', type: 'cell' },
184+
];
185+
186+
const PREFIX = {
187+
SIGNED_EXTERNAL: 0x7369676e,
188+
SEND_MESSAGE: 0x0ec3c86d,
189+
};
190+
191+
const WALLET_V5R1_CODE =
192+
'te6cckECFAEAAoEAART/APSkE/S88sgLAQIBIAINAgFIAwQC3NAg10nBIJFbj2Mg1wsfIIIQZXh0br0hghBzaW50vbCSXwPgghBleHRuuo60gCDXIQHQdNch+kAw+kT4KPpEMFi9kVvg7UTQgQFB1yH0BYMH9A5voTGRMOGAQNchcH/bPOAxINdJgQKAuZEw4HDiEA8CASAFDAIBIAYJAgFuBwgAGa3OdqJoQCDrkOuF/8AAGa8d9qJoQBDrkOuFj8ACAUgKCwAXsyX7UTQcdch1wsfgABGyYvtRNDXCgCAAGb5fD2omhAgKDrkPoCwBAvIOAR4g1wsfghBzaWduuvLgin8PAeaO8O2i7fshgwjXIgKDCNcjIIAg1yHTH9Mf0x/tRNDSANMfINMf0//XCgAK+QFAzPkQmiiUXwrbMeHywIffArNQB7Dy0IRRJbry4IVQNrry4Ib4I7vy0IgikvgA3gGkf8jKAMsfAc8Wye1UIJL4D95w2zzYEAP27aLt+wL0BCFukmwhjkwCIdc5MHCUIccAs44tAdcoIHYeQ2wg10nACPLgkyDXSsAC8uCTINcdBscSwgBSMLDy0InXTNc5MAGk6GwShAe78uCT10rAAPLgk+1V4tIAAcAAkVvg69csCBQgkXCWAdcsCBwS4lIQseMPINdKERITAJYB+kAB+kT4KPpEMFi68uCR7UTQgQFB1xj0BQSdf8jKAEAEgwf0U/Lgi44UA4MH9Fvy4Iwi1woAIW4Bs7Dy0JDiyFADzxYS9ADJ7VQAcjDXLAgkji0h8uCS0gDtRNDSAFETuvLQj1RQMJExnAGBAUDXIdcKAPLgjuLIygBYzxbJ7VST8sCN4gAQk1vbMeHXTNC01sNe';
193+
194+
const WALLET_V5R1_ABI = `{
195+
"ABI version": 2,
196+
"version": "2.3",
197+
"functions": [{
198+
"name": "sendTransaction",
199+
"id": "0x7369676E",
200+
"inputs": [
201+
{"name":"walletId","type":"uint32"},
202+
{"name":"validUntil","type":"uint32"},
203+
{"name":"seqno","type":"uint32"},
204+
{"name":"actions","type":"optional(cell)"},
205+
{"name":"extended","type":"optional(cell)"},
206+
{"name":"signatureHigh","type":"uint256"},
207+
{"name":"signatureLow","type":"uint256"}
208+
],
209+
"outputs": []
210+
}],
211+
"events": []
212+
}`;

src/client/AccountsStorage/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export { GenericAccount, MsigAccount } from './Generic';
1111
export { WalletV3Account } from './WalletV3';
1212
export { HighloadWalletV2 } from './HighloadWalletV2';
1313
export { EverWalletAccount } from './EverWallet';
14+
export { WalletV5R1Account } from './WalletV5R1';
1415

1516
/**
1617
* @category AccountsStorage

0 commit comments

Comments
 (0)