@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.
Files changed (106) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +9 -0
  3. package/LICENSE +202 -0
  4. package/dist/envelope.d.ts +34 -0
  5. package/dist/envelope.d.ts.map +1 -0
  6. package/dist/envelope.js +96 -0
  7. package/dist/index.d.ts +6 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +5 -0
  10. package/dist/relayer/index.d.ts +4 -0
  11. package/dist/relayer/index.d.ts.map +1 -0
  12. package/dist/relayer/index.js +3 -0
  13. package/dist/relayer/local.d.ts +28 -0
  14. package/dist/relayer/local.d.ts.map +1 -0
  15. package/dist/relayer/local.js +101 -0
  16. package/dist/relayer/pk-relayer.d.ts +18 -0
  17. package/dist/relayer/pk-relayer.d.ts.map +1 -0
  18. package/dist/relayer/pk-relayer.js +88 -0
  19. package/dist/relayer/relayer.d.ts +39 -0
  20. package/dist/relayer/relayer.d.ts.map +1 -0
  21. package/dist/relayer/relayer.js +1 -0
  22. package/dist/signers/index.d.ts +23 -0
  23. package/dist/signers/index.d.ts.map +1 -0
  24. package/dist/signers/index.js +10 -0
  25. package/dist/signers/passkey.d.ts +41 -0
  26. package/dist/signers/passkey.d.ts.map +1 -0
  27. package/dist/signers/passkey.js +196 -0
  28. package/dist/signers/pk/encrypted.d.ts +37 -0
  29. package/dist/signers/pk/encrypted.d.ts.map +1 -0
  30. package/dist/signers/pk/encrypted.js +123 -0
  31. package/dist/signers/pk/index.d.ts +35 -0
  32. package/dist/signers/pk/index.d.ts.map +1 -0
  33. package/dist/signers/pk/index.js +51 -0
  34. package/dist/signers/session/explicit.d.ts +18 -0
  35. package/dist/signers/session/explicit.d.ts.map +1 -0
  36. package/dist/signers/session/explicit.js +126 -0
  37. package/dist/signers/session/implicit.d.ts +20 -0
  38. package/dist/signers/session/implicit.d.ts.map +1 -0
  39. package/dist/signers/session/implicit.js +120 -0
  40. package/dist/signers/session/index.d.ts +4 -0
  41. package/dist/signers/session/index.d.ts.map +1 -0
  42. package/dist/signers/session/index.js +3 -0
  43. package/dist/signers/session/session.d.ts +11 -0
  44. package/dist/signers/session/session.d.ts.map +1 -0
  45. package/dist/signers/session/session.js +1 -0
  46. package/dist/signers/session-manager.d.ts +33 -0
  47. package/dist/signers/session-manager.d.ts.map +1 -0
  48. package/dist/signers/session-manager.js +181 -0
  49. package/dist/state/cached.d.ts +59 -0
  50. package/dist/state/cached.d.ts.map +1 -0
  51. package/dist/state/cached.js +157 -0
  52. package/dist/state/index.d.ts +61 -0
  53. package/dist/state/index.d.ts.map +1 -0
  54. package/dist/state/index.js +4 -0
  55. package/dist/state/local/index.d.ts +98 -0
  56. package/dist/state/local/index.d.ts.map +1 -0
  57. package/dist/state/local/index.js +247 -0
  58. package/dist/state/local/indexed-db.d.ts +41 -0
  59. package/dist/state/local/indexed-db.d.ts.map +1 -0
  60. package/dist/state/local/indexed-db.js +149 -0
  61. package/dist/state/local/memory.d.ts +41 -0
  62. package/dist/state/local/memory.d.ts.map +1 -0
  63. package/dist/state/local/memory.js +77 -0
  64. package/dist/state/remote/dev-http.d.ts +57 -0
  65. package/dist/state/remote/dev-http.d.ts.map +1 -0
  66. package/dist/state/remote/dev-http.js +162 -0
  67. package/dist/state/remote/index.d.ts +2 -0
  68. package/dist/state/remote/index.d.ts.map +1 -0
  69. package/dist/state/remote/index.js +1 -0
  70. package/dist/state/utils.d.ts +12 -0
  71. package/dist/state/utils.d.ts.map +1 -0
  72. package/dist/state/utils.js +29 -0
  73. package/dist/wallet.d.ts +58 -0
  74. package/dist/wallet.d.ts.map +1 -0
  75. package/dist/wallet.js +306 -0
  76. package/package.json +33 -0
  77. package/src/envelope.ts +148 -0
  78. package/src/index.ts +6 -0
  79. package/src/relayer/index.ts +3 -0
  80. package/src/relayer/local.ts +125 -0
  81. package/src/relayer/pk-relayer.ts +110 -0
  82. package/src/relayer/relayer.ts +52 -0
  83. package/src/signers/index.ts +44 -0
  84. package/src/signers/passkey.ts +284 -0
  85. package/src/signers/pk/encrypted.ts +153 -0
  86. package/src/signers/pk/index.ts +77 -0
  87. package/src/signers/session/explicit.ts +173 -0
  88. package/src/signers/session/implicit.ts +145 -0
  89. package/src/signers/session/index.ts +3 -0
  90. package/src/signers/session/session.ts +26 -0
  91. package/src/signers/session-manager.ts +241 -0
  92. package/src/state/cached.ts +233 -0
  93. package/src/state/index.ts +85 -0
  94. package/src/state/local/index.ts +422 -0
  95. package/src/state/local/indexed-db.ts +204 -0
  96. package/src/state/local/memory.ts +126 -0
  97. package/src/state/remote/dev-http.ts +253 -0
  98. package/src/state/remote/index.ts +1 -0
  99. package/src/state/utils.ts +50 -0
  100. package/src/wallet.ts +390 -0
  101. package/test/constants.ts +15 -0
  102. package/test/session-manager.test.ts +451 -0
  103. package/test/setup.ts +63 -0
  104. package/test/wallet.test.ts +90 -0
  105. package/tsconfig.json +10 -0
  106. 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
+ }
@@ -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,6 @@
1
+ export * from './wallet.js'
2
+
3
+ export * as Signers from './signers/index.js'
4
+ export * as State from './state/index.js'
5
+ export * as Relayer from './relayer/index.js'
6
+ export * as Envelope from './envelope.js'
@@ -0,0 +1,3 @@
1
+ export * from './relayer.js'
2
+ export * as Local from './local.js'
3
+ export * as Pk from './pk-relayer.js'
@@ -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
+ }