@0xsequence/wallet-core 0.0.0-20250520201059
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +202 -0
- package/dist/envelope.d.ts +34 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +96 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/relayer/index.d.ts +4 -0
- package/dist/relayer/index.d.ts.map +1 -0
- package/dist/relayer/index.js +3 -0
- package/dist/relayer/local.d.ts +28 -0
- package/dist/relayer/local.d.ts.map +1 -0
- package/dist/relayer/local.js +101 -0
- package/dist/relayer/pk-relayer.d.ts +18 -0
- package/dist/relayer/pk-relayer.d.ts.map +1 -0
- package/dist/relayer/pk-relayer.js +88 -0
- package/dist/relayer/relayer.d.ts +39 -0
- package/dist/relayer/relayer.d.ts.map +1 -0
- package/dist/relayer/relayer.js +1 -0
- package/dist/signers/index.d.ts +23 -0
- package/dist/signers/index.d.ts.map +1 -0
- package/dist/signers/index.js +10 -0
- package/dist/signers/passkey.d.ts +41 -0
- package/dist/signers/passkey.d.ts.map +1 -0
- package/dist/signers/passkey.js +196 -0
- package/dist/signers/pk/encrypted.d.ts +37 -0
- package/dist/signers/pk/encrypted.d.ts.map +1 -0
- package/dist/signers/pk/encrypted.js +123 -0
- package/dist/signers/pk/index.d.ts +35 -0
- package/dist/signers/pk/index.d.ts.map +1 -0
- package/dist/signers/pk/index.js +51 -0
- package/dist/signers/session/explicit.d.ts +18 -0
- package/dist/signers/session/explicit.d.ts.map +1 -0
- package/dist/signers/session/explicit.js +126 -0
- package/dist/signers/session/implicit.d.ts +20 -0
- package/dist/signers/session/implicit.d.ts.map +1 -0
- package/dist/signers/session/implicit.js +120 -0
- package/dist/signers/session/index.d.ts +4 -0
- package/dist/signers/session/index.d.ts.map +1 -0
- package/dist/signers/session/index.js +3 -0
- package/dist/signers/session/session.d.ts +11 -0
- package/dist/signers/session/session.d.ts.map +1 -0
- package/dist/signers/session/session.js +1 -0
- package/dist/signers/session-manager.d.ts +33 -0
- package/dist/signers/session-manager.d.ts.map +1 -0
- package/dist/signers/session-manager.js +181 -0
- package/dist/state/cached.d.ts +59 -0
- package/dist/state/cached.d.ts.map +1 -0
- package/dist/state/cached.js +157 -0
- package/dist/state/index.d.ts +61 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +4 -0
- package/dist/state/local/index.d.ts +98 -0
- package/dist/state/local/index.d.ts.map +1 -0
- package/dist/state/local/index.js +247 -0
- package/dist/state/local/indexed-db.d.ts +41 -0
- package/dist/state/local/indexed-db.d.ts.map +1 -0
- package/dist/state/local/indexed-db.js +149 -0
- package/dist/state/local/memory.d.ts +41 -0
- package/dist/state/local/memory.d.ts.map +1 -0
- package/dist/state/local/memory.js +77 -0
- package/dist/state/remote/dev-http.d.ts +57 -0
- package/dist/state/remote/dev-http.d.ts.map +1 -0
- package/dist/state/remote/dev-http.js +162 -0
- package/dist/state/remote/index.d.ts +2 -0
- package/dist/state/remote/index.d.ts.map +1 -0
- package/dist/state/remote/index.js +1 -0
- package/dist/state/utils.d.ts +12 -0
- package/dist/state/utils.d.ts.map +1 -0
- package/dist/state/utils.js +29 -0
- package/dist/wallet.d.ts +58 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +306 -0
- package/package.json +33 -0
- package/src/envelope.ts +148 -0
- package/src/index.ts +6 -0
- package/src/relayer/index.ts +3 -0
- package/src/relayer/local.ts +125 -0
- package/src/relayer/pk-relayer.ts +110 -0
- package/src/relayer/relayer.ts +52 -0
- package/src/signers/index.ts +44 -0
- package/src/signers/passkey.ts +284 -0
- package/src/signers/pk/encrypted.ts +153 -0
- package/src/signers/pk/index.ts +77 -0
- package/src/signers/session/explicit.ts +173 -0
- package/src/signers/session/implicit.ts +145 -0
- package/src/signers/session/index.ts +3 -0
- package/src/signers/session/session.ts +26 -0
- package/src/signers/session-manager.ts +241 -0
- package/src/state/cached.ts +233 -0
- package/src/state/index.ts +85 -0
- package/src/state/local/index.ts +422 -0
- package/src/state/local/indexed-db.ts +204 -0
- package/src/state/local/memory.ts +126 -0
- package/src/state/remote/dev-http.ts +253 -0
- package/src/state/remote/index.ts +1 -0
- package/src/state/utils.ts +50 -0
- package/src/wallet.ts +390 -0
- package/test/constants.ts +15 -0
- package/test/session-manager.test.ts +451 -0
- package/test/setup.ts +63 -0
- package/test/wallet.test.ts +90 -0
- package/tsconfig.json +10 -0
- package/vitest.config.ts +9 -0
package/dist/wallet.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { Config, Constants, Context, Erc6492, Payload, Address as SequenceAddress, Signature as SequenceSignature, } from '@0xsequence/wallet-primitives';
|
|
2
|
+
import { AbiFunction, Address, Bytes, Hex, TypedData } from 'ox';
|
|
3
|
+
import * as Envelope from './envelope.js';
|
|
4
|
+
import * as State from './state/index.js';
|
|
5
|
+
export const DefaultWalletOptions = {
|
|
6
|
+
context: Context.Dev1,
|
|
7
|
+
stateProvider: new State.Local.Provider(),
|
|
8
|
+
guest: Constants.DefaultGuest,
|
|
9
|
+
};
|
|
10
|
+
export class Wallet {
|
|
11
|
+
address;
|
|
12
|
+
context;
|
|
13
|
+
guest;
|
|
14
|
+
stateProvider;
|
|
15
|
+
constructor(address, options) {
|
|
16
|
+
this.address = address;
|
|
17
|
+
const combinedOptions = { ...DefaultWalletOptions, ...options };
|
|
18
|
+
this.context = combinedOptions.context;
|
|
19
|
+
this.guest = combinedOptions.guest;
|
|
20
|
+
this.stateProvider = combinedOptions.stateProvider;
|
|
21
|
+
}
|
|
22
|
+
static async fromConfiguration(configuration, options) {
|
|
23
|
+
const merged = { ...DefaultWalletOptions, ...options };
|
|
24
|
+
//FIXME Validate configuration (weights not too large, total weights above threshold, etc)
|
|
25
|
+
await merged.stateProvider.saveWallet(configuration, merged.context);
|
|
26
|
+
return new Wallet(SequenceAddress.from(configuration, merged.context), merged);
|
|
27
|
+
}
|
|
28
|
+
async isDeployed(provider) {
|
|
29
|
+
return (await provider.request({ method: 'eth_getCode', params: [this.address, 'pending'] })) !== '0x';
|
|
30
|
+
}
|
|
31
|
+
async buildDeployTransaction() {
|
|
32
|
+
const deployInformation = await this.stateProvider.getDeploy(this.address);
|
|
33
|
+
if (!deployInformation) {
|
|
34
|
+
throw new Error(`cannot find deploy information for ${this.address}`);
|
|
35
|
+
}
|
|
36
|
+
return Erc6492.deploy(deployInformation.imageHash, deployInformation.context);
|
|
37
|
+
}
|
|
38
|
+
async prepareUpdate(configuration) {
|
|
39
|
+
const imageHash = Config.hashConfiguration(configuration);
|
|
40
|
+
const blankEvelope = (await Promise.all([
|
|
41
|
+
this.prepareBlankEnvelope(0n),
|
|
42
|
+
// TODO: Add save configuration
|
|
43
|
+
this.stateProvider.saveWallet(configuration, this.context),
|
|
44
|
+
]))[0];
|
|
45
|
+
return {
|
|
46
|
+
...blankEvelope,
|
|
47
|
+
payload: Payload.fromConfigUpdate(Bytes.toHex(imageHash)),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async submitUpdate(envelope, options) {
|
|
51
|
+
const [status, newConfig] = await Promise.all([
|
|
52
|
+
this.getStatus(),
|
|
53
|
+
this.stateProvider.getConfiguration(envelope.payload.imageHash),
|
|
54
|
+
]);
|
|
55
|
+
if (!newConfig) {
|
|
56
|
+
throw new Error(`cannot find configuration details for ${envelope.payload.imageHash}`);
|
|
57
|
+
}
|
|
58
|
+
// Verify the new configuration is valid
|
|
59
|
+
const updatedEnvelope = { ...envelope, configuration: status.configuration };
|
|
60
|
+
const { weight, threshold } = Envelope.weightOf(updatedEnvelope);
|
|
61
|
+
if (weight < threshold) {
|
|
62
|
+
throw new Error('insufficient weight in envelope');
|
|
63
|
+
}
|
|
64
|
+
const signature = Envelope.encodeSignature(updatedEnvelope);
|
|
65
|
+
await this.stateProvider.saveUpdate(this.address, newConfig, signature);
|
|
66
|
+
if (options?.validateSave) {
|
|
67
|
+
const status = await this.getStatus();
|
|
68
|
+
if (Hex.from(Config.hashConfiguration(status.configuration)) !== envelope.payload.imageHash) {
|
|
69
|
+
throw new Error('configuration not saved');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async getStatus(provider) {
|
|
74
|
+
let isDeployed = false;
|
|
75
|
+
let implementation;
|
|
76
|
+
let stage;
|
|
77
|
+
let chainId;
|
|
78
|
+
let imageHash;
|
|
79
|
+
let updates = [];
|
|
80
|
+
let onChainImageHash;
|
|
81
|
+
if (provider) {
|
|
82
|
+
// Get chain ID, deployment status, and implementation
|
|
83
|
+
const requests = await Promise.all([
|
|
84
|
+
provider.request({ method: 'eth_chainId' }),
|
|
85
|
+
this.isDeployed(provider),
|
|
86
|
+
provider
|
|
87
|
+
.request({
|
|
88
|
+
method: 'eth_call',
|
|
89
|
+
params: [{ to: this.address, data: AbiFunction.encodeData(Constants.GET_IMPLEMENTATION) }],
|
|
90
|
+
})
|
|
91
|
+
.then((res) => {
|
|
92
|
+
const address = `0x${res.slice(-40)}`;
|
|
93
|
+
Address.assert(address, { strict: false });
|
|
94
|
+
return address;
|
|
95
|
+
})
|
|
96
|
+
.catch(() => undefined),
|
|
97
|
+
]);
|
|
98
|
+
chainId = BigInt(requests[0]);
|
|
99
|
+
isDeployed = requests[1];
|
|
100
|
+
implementation = requests[2];
|
|
101
|
+
// Determine stage based on implementation address
|
|
102
|
+
if (implementation) {
|
|
103
|
+
if (Address.isEqual(implementation, this.context.stage1)) {
|
|
104
|
+
stage = 'stage1';
|
|
105
|
+
}
|
|
106
|
+
else if (Address.isEqual(implementation, this.context.stage2)) {
|
|
107
|
+
stage = 'stage2';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Get image hash and updates
|
|
111
|
+
if (isDeployed && stage === 'stage2') {
|
|
112
|
+
// For deployed stage2 wallets, get the image hash from the contract
|
|
113
|
+
onChainImageHash = await provider.request({
|
|
114
|
+
method: 'eth_call',
|
|
115
|
+
params: [{ to: this.address, data: AbiFunction.encodeData(Constants.IMAGE_HASH) }],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// For non-deployed or stage1 wallets, get the deploy hash
|
|
120
|
+
const deployInformation = await this.stateProvider.getDeploy(this.address);
|
|
121
|
+
if (!deployInformation) {
|
|
122
|
+
throw new Error(`cannot find deploy information for ${this.address}`);
|
|
123
|
+
}
|
|
124
|
+
onChainImageHash = deployInformation.imageHash;
|
|
125
|
+
}
|
|
126
|
+
// Get configuration updates
|
|
127
|
+
updates = await this.stateProvider.getConfigurationUpdates(this.address, onChainImageHash);
|
|
128
|
+
imageHash = updates[updates.length - 1]?.imageHash ?? onChainImageHash;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Without a provider, we can only get information from the state provider
|
|
132
|
+
const deployInformation = await this.stateProvider.getDeploy(this.address);
|
|
133
|
+
if (!deployInformation) {
|
|
134
|
+
throw new Error(`cannot find deploy information for ${this.address}`);
|
|
135
|
+
}
|
|
136
|
+
updates = await this.stateProvider.getConfigurationUpdates(this.address, deployInformation.imageHash);
|
|
137
|
+
imageHash = updates[updates.length - 1]?.imageHash ?? deployInformation.imageHash;
|
|
138
|
+
}
|
|
139
|
+
// Get the current configuration
|
|
140
|
+
const configuration = await this.stateProvider.getConfiguration(imageHash);
|
|
141
|
+
if (!configuration) {
|
|
142
|
+
throw new Error(`cannot find configuration details for ${this.address}`);
|
|
143
|
+
}
|
|
144
|
+
if (provider) {
|
|
145
|
+
return {
|
|
146
|
+
address: this.address,
|
|
147
|
+
isDeployed,
|
|
148
|
+
implementation,
|
|
149
|
+
stage,
|
|
150
|
+
configuration,
|
|
151
|
+
imageHash,
|
|
152
|
+
pendingUpdates: [...updates].reverse(),
|
|
153
|
+
chainId,
|
|
154
|
+
onChainImageHash: onChainImageHash,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
return {
|
|
159
|
+
address: this.address,
|
|
160
|
+
isDeployed,
|
|
161
|
+
implementation,
|
|
162
|
+
stage,
|
|
163
|
+
configuration,
|
|
164
|
+
imageHash,
|
|
165
|
+
pendingUpdates: [...updates].reverse(),
|
|
166
|
+
chainId,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async getNonce(provider, space) {
|
|
171
|
+
const result = await provider.request({
|
|
172
|
+
method: 'eth_call',
|
|
173
|
+
params: [{ to: this.address, data: AbiFunction.encodeData(Constants.READ_NONCE, [space]) }],
|
|
174
|
+
});
|
|
175
|
+
if (result === '0x' || result.length === 0) {
|
|
176
|
+
return 0n;
|
|
177
|
+
}
|
|
178
|
+
return BigInt(result);
|
|
179
|
+
}
|
|
180
|
+
async prepareTransaction(provider, calls, options) {
|
|
181
|
+
const space = options?.space ?? 0n;
|
|
182
|
+
const [chainId, nonce] = await Promise.all([
|
|
183
|
+
provider.request({ method: 'eth_chainId' }),
|
|
184
|
+
this.getNonce(provider, space),
|
|
185
|
+
]);
|
|
186
|
+
// If the latest configuration does not match the onchain configuration
|
|
187
|
+
// then we bundle the update into the transaction envelope
|
|
188
|
+
if (!options?.noConfigUpdate) {
|
|
189
|
+
const status = await this.getStatus(provider);
|
|
190
|
+
if (status.imageHash !== status.onChainImageHash) {
|
|
191
|
+
calls.push({
|
|
192
|
+
to: this.address,
|
|
193
|
+
value: 0n,
|
|
194
|
+
data: AbiFunction.encodeData(Constants.UPDATE_IMAGE_HASH, [status.imageHash]),
|
|
195
|
+
gasLimit: 0n,
|
|
196
|
+
delegateCall: false,
|
|
197
|
+
onlyFallback: false,
|
|
198
|
+
behaviorOnError: 'revert',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
payload: {
|
|
204
|
+
type: 'call',
|
|
205
|
+
space,
|
|
206
|
+
nonce,
|
|
207
|
+
calls,
|
|
208
|
+
},
|
|
209
|
+
...(await this.prepareBlankEnvelope(BigInt(chainId))),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
async buildTransaction(provider, envelope) {
|
|
213
|
+
const status = await this.getStatus(provider);
|
|
214
|
+
const updatedEnvelope = { ...envelope, configuration: status.configuration };
|
|
215
|
+
const { weight, threshold } = Envelope.weightOf(updatedEnvelope);
|
|
216
|
+
if (weight < threshold) {
|
|
217
|
+
throw new Error('insufficient weight in envelope');
|
|
218
|
+
}
|
|
219
|
+
const signature = Envelope.encodeSignature(updatedEnvelope);
|
|
220
|
+
if (status.isDeployed) {
|
|
221
|
+
return {
|
|
222
|
+
to: this.address,
|
|
223
|
+
data: AbiFunction.encodeData(Constants.EXECUTE, [
|
|
224
|
+
Bytes.toHex(Payload.encode(envelope.payload)),
|
|
225
|
+
Bytes.toHex(SequenceSignature.encodeSignature({
|
|
226
|
+
...signature,
|
|
227
|
+
suffix: status.pendingUpdates.map(({ signature }) => signature),
|
|
228
|
+
})),
|
|
229
|
+
]),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const deploy = await this.buildDeployTransaction();
|
|
234
|
+
return {
|
|
235
|
+
to: this.guest,
|
|
236
|
+
data: Bytes.toHex(Payload.encode({
|
|
237
|
+
type: 'call',
|
|
238
|
+
space: 0n,
|
|
239
|
+
nonce: 0n,
|
|
240
|
+
calls: [
|
|
241
|
+
{
|
|
242
|
+
to: deploy.to,
|
|
243
|
+
value: 0n,
|
|
244
|
+
data: deploy.data,
|
|
245
|
+
gasLimit: 0n,
|
|
246
|
+
delegateCall: false,
|
|
247
|
+
onlyFallback: false,
|
|
248
|
+
behaviorOnError: 'revert',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
to: this.address,
|
|
252
|
+
value: 0n,
|
|
253
|
+
data: AbiFunction.encodeData(Constants.EXECUTE, [
|
|
254
|
+
Bytes.toHex(Payload.encode(envelope.payload)),
|
|
255
|
+
Bytes.toHex(SequenceSignature.encodeSignature({
|
|
256
|
+
...signature,
|
|
257
|
+
suffix: status.pendingUpdates.map(({ signature }) => signature),
|
|
258
|
+
})),
|
|
259
|
+
]),
|
|
260
|
+
gasLimit: 0n,
|
|
261
|
+
delegateCall: false,
|
|
262
|
+
onlyFallback: false,
|
|
263
|
+
behaviorOnError: 'revert',
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
})),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async prepareMessageSignature(message, chainId) {
|
|
271
|
+
let encodedMessage;
|
|
272
|
+
if (typeof message !== 'string') {
|
|
273
|
+
encodedMessage = TypedData.encode(message);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
let hexMessage = Hex.validate(message) ? message : Hex.fromString(message);
|
|
277
|
+
const messageSize = Hex.size(hexMessage);
|
|
278
|
+
encodedMessage = Hex.concat(Hex.fromString(`${`\x19Ethereum Signed Message:\n${messageSize}`}`), hexMessage);
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
...(await this.prepareBlankEnvelope(chainId)),
|
|
282
|
+
payload: Payload.fromMessage(encodedMessage),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async buildMessageSignature(envelope, provider) {
|
|
286
|
+
const status = await this.getStatus(provider);
|
|
287
|
+
const signature = Envelope.encodeSignature(envelope);
|
|
288
|
+
if (!status.isDeployed) {
|
|
289
|
+
const deployTransaction = await this.buildDeployTransaction();
|
|
290
|
+
signature.erc6492 = { to: deployTransaction.to, data: Bytes.fromHex(deployTransaction.data) };
|
|
291
|
+
}
|
|
292
|
+
const encoded = SequenceSignature.encodeSignature({
|
|
293
|
+
...signature,
|
|
294
|
+
suffix: status.pendingUpdates.map(({ signature }) => signature),
|
|
295
|
+
});
|
|
296
|
+
return encoded;
|
|
297
|
+
}
|
|
298
|
+
async prepareBlankEnvelope(chainId) {
|
|
299
|
+
const status = await this.getStatus();
|
|
300
|
+
return {
|
|
301
|
+
wallet: this.address,
|
|
302
|
+
chainId: chainId,
|
|
303
|
+
configuration: status.configuration,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0xsequence/wallet-core",
|
|
3
|
+
"version": "0.0.0-20250520201059",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"private": false,
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.13.9",
|
|
18
|
+
"dotenv": "^16.4.7",
|
|
19
|
+
"fake-indexeddb": "^6.0.0",
|
|
20
|
+
"typescript": "^5.7.3",
|
|
21
|
+
"vitest": "^3.1.2",
|
|
22
|
+
"@repo/typescript-config": "^0.0.0-20250520201059"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"ox": "^0.7.0",
|
|
26
|
+
"@0xsequence/wallet-primitives": "^0.0.0-20250520201059"
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"dev": "tsc --watch",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/envelope.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Config, Payload, Signature } from '@0xsequence/wallet-primitives'
|
|
2
|
+
import { Address, Hex } from 'ox'
|
|
3
|
+
|
|
4
|
+
export type Envelope<T extends Payload.Payload> = {
|
|
5
|
+
readonly wallet: Address.Address
|
|
6
|
+
readonly chainId: bigint
|
|
7
|
+
readonly configuration: Config.Config
|
|
8
|
+
readonly payload: T
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Signature = {
|
|
12
|
+
address: Address.Address
|
|
13
|
+
signature: Signature.SignatureOfSignerLeaf
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Address not included as it is included in the signature
|
|
17
|
+
export type SapientSignature = {
|
|
18
|
+
imageHash: Hex.Hex
|
|
19
|
+
signature: Signature.SignatureOfSapientSignerLeaf
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isSignature(sig: any): sig is Signature {
|
|
23
|
+
return typeof sig === 'object' && 'address' in sig && 'signature' in sig && !('imageHash' in sig)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isSapientSignature(sig: any): sig is SapientSignature {
|
|
27
|
+
return typeof sig === 'object' && 'signature' in sig && 'imageHash' in sig
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Signed<T extends Payload.Payload> = Envelope<T> & {
|
|
31
|
+
signatures: (Signature | SapientSignature)[]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function signatureForLeaf(envelope: Signed<Payload.Payload>, leaf: Config.Leaf) {
|
|
35
|
+
if (Config.isSignerLeaf(leaf)) {
|
|
36
|
+
return envelope.signatures.find((sig) => isSignature(sig) && Address.isEqual(sig.address, leaf.address))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Config.isSapientSignerLeaf(leaf)) {
|
|
40
|
+
return envelope.signatures.find(
|
|
41
|
+
(sig) =>
|
|
42
|
+
isSapientSignature(sig) &&
|
|
43
|
+
sig.imageHash === leaf.imageHash &&
|
|
44
|
+
Address.isEqual(sig.signature.address, leaf.address),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function weightOf(envelope: Signed<Payload.Payload>): { weight: bigint; threshold: bigint } {
|
|
52
|
+
const { maxWeight } = Config.getWeight(envelope.configuration, (s) => !!signatureForLeaf(envelope, s))
|
|
53
|
+
return {
|
|
54
|
+
weight: maxWeight,
|
|
55
|
+
threshold: envelope.configuration.threshold,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function reachedThreshold(envelope: Signed<Payload.Payload>): boolean {
|
|
60
|
+
const { weight, threshold } = weightOf(envelope)
|
|
61
|
+
return weight >= threshold
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function encodeSignature(envelope: Signed<Payload.Payload>): Signature.RawSignature {
|
|
65
|
+
const topology = Signature.fillLeaves(
|
|
66
|
+
envelope.configuration.topology,
|
|
67
|
+
(s) => signatureForLeaf(envelope, s)?.signature,
|
|
68
|
+
)
|
|
69
|
+
return {
|
|
70
|
+
noChainId: envelope.chainId === 0n,
|
|
71
|
+
configuration: { ...envelope.configuration, topology },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function toSigned<T extends Payload.Payload>(
|
|
76
|
+
envelope: Envelope<T>,
|
|
77
|
+
signatures: (Signature | SapientSignature)[] = [],
|
|
78
|
+
): Signed<T> {
|
|
79
|
+
return {
|
|
80
|
+
...envelope,
|
|
81
|
+
signatures,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function addSignature(
|
|
86
|
+
envelope: Signed<Payload.Payload>,
|
|
87
|
+
signature: Signature | SapientSignature,
|
|
88
|
+
args?: { replace?: boolean },
|
|
89
|
+
) {
|
|
90
|
+
if (isSapientSignature(signature)) {
|
|
91
|
+
// Find if the signature already exists in envelope
|
|
92
|
+
const prev = envelope.signatures.find(
|
|
93
|
+
(sig) =>
|
|
94
|
+
isSapientSignature(sig) &&
|
|
95
|
+
Address.isEqual(sig.signature.address, signature.signature.address) &&
|
|
96
|
+
sig.imageHash === signature.imageHash,
|
|
97
|
+
) as SapientSignature | undefined
|
|
98
|
+
|
|
99
|
+
if (prev) {
|
|
100
|
+
// If the signatures are identical, then we can do nothing
|
|
101
|
+
if (prev.signature.data === signature.signature.data) {
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If not and we are replacing, then remove the previous signature
|
|
106
|
+
if (args?.replace) {
|
|
107
|
+
envelope.signatures = envelope.signatures.filter((sig) => sig !== prev)
|
|
108
|
+
} else {
|
|
109
|
+
throw new Error('Signature already defined for signer')
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
envelope.signatures.push(signature)
|
|
114
|
+
} else if (isSignature(signature)) {
|
|
115
|
+
// Find if the signature already exists in envelope
|
|
116
|
+
const prev = envelope.signatures.find(
|
|
117
|
+
(sig) => isSignature(sig) && Address.isEqual(sig.address, signature.address),
|
|
118
|
+
) as Signature | undefined
|
|
119
|
+
|
|
120
|
+
if (prev) {
|
|
121
|
+
// If the signatures are identical, then we can do nothing
|
|
122
|
+
if (prev.signature.type === 'erc1271' && signature.signature.type === 'erc1271') {
|
|
123
|
+
if (prev.signature.data === signature.signature.data) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
} else if (prev.signature.type !== 'erc1271' && signature.signature.type !== 'erc1271') {
|
|
127
|
+
if (prev.signature.r === signature.signature.r && prev.signature.s === signature.signature.s) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// If not and we are replacing, then remove the previous signature
|
|
133
|
+
if (args?.replace) {
|
|
134
|
+
envelope.signatures = envelope.signatures.filter((sig) => sig !== prev)
|
|
135
|
+
} else {
|
|
136
|
+
throw new Error('Signature already defined for signer')
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
envelope.signatures.push(signature)
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error('Unsupported signature type')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isSigned(envelope: Envelope<Payload.Payload>): envelope is Signed<Payload.Payload> {
|
|
147
|
+
return typeof envelope === 'object' && 'signatures' in envelope
|
|
148
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Constants, Payload } from '@0xsequence/wallet-primitives'
|
|
2
|
+
import { AbiFunction, Address, Bytes, Hex, TransactionReceipt } from 'ox'
|
|
3
|
+
import { FeeOption, FeeQuote, OperationStatus, Relayer } from './relayer.js'
|
|
4
|
+
|
|
5
|
+
type GenericProviderTransactionReceipt = 'success' | 'failed' | 'unknown'
|
|
6
|
+
|
|
7
|
+
export interface GenericProvider {
|
|
8
|
+
sendTransaction(args: { to: string; data: string }, chainId: bigint): Promise<string>
|
|
9
|
+
getTransactionReceipt(txHash: string, chainId: bigint): Promise<GenericProviderTransactionReceipt>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class LocalRelayer implements Relayer {
|
|
13
|
+
public readonly id = 'local'
|
|
14
|
+
|
|
15
|
+
constructor(public readonly provider: GenericProvider) {}
|
|
16
|
+
|
|
17
|
+
static createFromWindow(window: Window): LocalRelayer | undefined {
|
|
18
|
+
const eth = (window as any).ethereum
|
|
19
|
+
if (!eth) {
|
|
20
|
+
console.warn('Window.ethereum not found, skipping local relayer')
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trySwitchChain = async (chainId: bigint) => {
|
|
25
|
+
try {
|
|
26
|
+
await eth.request({
|
|
27
|
+
method: 'wallet_switchEthereumChain',
|
|
28
|
+
params: [
|
|
29
|
+
{
|
|
30
|
+
chainId: `0x${chainId.toString(16)}`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
} catch (error) {
|
|
35
|
+
// Log and continue
|
|
36
|
+
console.error('Error switching chain', error)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new LocalRelayer({
|
|
41
|
+
sendTransaction: async (args, chainId) => {
|
|
42
|
+
const accounts: string[] = await eth.request({ method: 'eth_requestAccounts' })
|
|
43
|
+
const from = accounts[0]
|
|
44
|
+
if (!from) {
|
|
45
|
+
console.warn('No account selected, skipping local relayer')
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
await trySwitchChain(chainId)
|
|
50
|
+
|
|
51
|
+
const tx = await eth.request({
|
|
52
|
+
method: 'eth_sendTransaction',
|
|
53
|
+
params: [
|
|
54
|
+
{
|
|
55
|
+
from,
|
|
56
|
+
to: args.to,
|
|
57
|
+
data: args.data,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
return tx
|
|
62
|
+
},
|
|
63
|
+
getTransactionReceipt: async (txHash, chainId) => {
|
|
64
|
+
await trySwitchChain(chainId)
|
|
65
|
+
|
|
66
|
+
const rpcReceipt = await eth.request({ method: 'eth_getTransactionReceipt', params: [txHash] })
|
|
67
|
+
if (rpcReceipt) {
|
|
68
|
+
const receipt = TransactionReceipt.fromRpc(rpcReceipt)
|
|
69
|
+
if (receipt?.status === 'success') {
|
|
70
|
+
return 'success'
|
|
71
|
+
} else if (receipt?.status === 'reverted') {
|
|
72
|
+
return 'failed'
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 'unknown'
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
feeOptions(
|
|
81
|
+
wallet: Address.Address,
|
|
82
|
+
chainId: bigint,
|
|
83
|
+
calls: Payload.Call[],
|
|
84
|
+
): Promise<{ options: FeeOption[]; quote?: FeeQuote }> {
|
|
85
|
+
return Promise.resolve({ options: [] })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private decodeCalls(data: Hex.Hex): Payload.Calls {
|
|
89
|
+
const executeSelector = AbiFunction.getSelector(Constants.EXECUTE)
|
|
90
|
+
|
|
91
|
+
let packedPayload
|
|
92
|
+
if (data.startsWith(executeSelector)) {
|
|
93
|
+
const decode = AbiFunction.decodeData(Constants.EXECUTE, data)
|
|
94
|
+
packedPayload = decode[0]
|
|
95
|
+
} else {
|
|
96
|
+
packedPayload = data
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Payload.decode(Bytes.fromHex(packedPayload))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async relay(to: Address.Address, data: Hex.Hex, chainId: bigint, _?: FeeQuote): Promise<{ opHash: Hex.Hex }> {
|
|
103
|
+
const txHash = await this.provider.sendTransaction(
|
|
104
|
+
{
|
|
105
|
+
to,
|
|
106
|
+
data,
|
|
107
|
+
},
|
|
108
|
+
chainId,
|
|
109
|
+
)
|
|
110
|
+
Hex.assert(txHash)
|
|
111
|
+
|
|
112
|
+
return { opHash: txHash }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async status(opHash: Hex.Hex, chainId: bigint): Promise<OperationStatus> {
|
|
116
|
+
const receipt = await this.provider.getTransactionReceipt(opHash, chainId)
|
|
117
|
+
if (receipt === 'unknown') {
|
|
118
|
+
// Could be pending but we don't know
|
|
119
|
+
return { status: 'unknown' }
|
|
120
|
+
}
|
|
121
|
+
return receipt === 'success'
|
|
122
|
+
? { status: 'confirmed', transactionHash: opHash }
|
|
123
|
+
: { status: 'failed', reason: 'failed' }
|
|
124
|
+
}
|
|
125
|
+
}
|