Skip to content

Commit 9fec65b

Browse files
committed
Add precondition checks in LocalRelayer and update exports in wallet module
1 parent ffaa911 commit 9fec65b

12 files changed

Lines changed: 818 additions & 3 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './intents'
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { Address, Bytes, ContractAddress, Hash } from 'ox'
2+
import { Context, Config, Payload } from '@0xsequence/wallet-primitives'
3+
4+
export type IntentOperation = {
5+
chainId: bigint
6+
space?: bigint
7+
nonce?: bigint
8+
calls: {
9+
to: Address.Address
10+
value: bigint
11+
data: Bytes.Bytes
12+
gasLimit: bigint
13+
delegateCall: boolean
14+
onlyFallback: boolean
15+
behaviorOnError: bigint
16+
}[]
17+
}
18+
19+
export function calculateIntentConfigurationAddress(
20+
operations: IntentOperation[],
21+
mainSigner: Address.Address,
22+
context: Context.Context,
23+
): Address.Address {
24+
// Create the intent configuration
25+
const config = createIntentConfiguration(operations, mainSigner)
26+
27+
// Calculate the image hash of the configuration
28+
const imageHash = Config.hashConfiguration(config)
29+
30+
// Calculate the counterfactual address using the image hash and context
31+
return ContractAddress.fromCreate2({
32+
from: context.factory,
33+
bytecodeHash: Hash.keccak256(
34+
Bytes.concat(Bytes.from(context.creationCode), Bytes.padLeft(Bytes.from(context.stage1), 32)),
35+
{ as: 'Bytes' },
36+
),
37+
salt: imageHash,
38+
})
39+
}
40+
41+
function createIntentConfiguration(operations: IntentOperation[], mainSigner: Address.Address): Config.Config {
42+
// Create the main signer leaf
43+
const mainSignerLeaf: Config.SignerLeaf = {
44+
type: 'signer',
45+
address: mainSigner,
46+
weight: 1n,
47+
}
48+
49+
// Create subdigest leaves for each operation
50+
const subdigestLeaves = operations.map((op) => {
51+
// Create the calls payload
52+
const payload: Payload.Calls = {
53+
type: 'call',
54+
space: op.space ?? 0n,
55+
nonce: op.nonce ?? 0n,
56+
calls: op.calls.map((call) => ({
57+
to: call.to,
58+
value: call.value,
59+
data: call.data,
60+
gasLimit: call.gasLimit,
61+
delegateCall: call.delegateCall,
62+
onlyFallback: call.onlyFallback,
63+
behaviorOnError: call.behaviorOnError === 0n ? 'ignore' : call.behaviorOnError === 1n ? 'revert' : 'abort',
64+
})),
65+
}
66+
67+
// Get the digest hash using Payload.hash
68+
const digest = Payload.hash(Address.from('0x0000000000000000000000000000000000000000'), op.chainId, payload)
69+
console.log('digest:', Bytes.toHex(digest))
70+
71+
// Create subdigest leaf
72+
return {
73+
type: 'any-address-subdigest',
74+
digest,
75+
} as Config.AnyAddressSubdigestLeaf
76+
})
77+
78+
// If there's only one operation, use its subdigest leaf directly
79+
if (subdigestLeaves.length === 1) {
80+
return {
81+
threshold: 1n,
82+
checkpoint: 0n,
83+
topology: [mainSignerLeaf, subdigestLeaves[0]] as Config.Topology,
84+
}
85+
}
86+
87+
// Otherwise, create a tree of subdigest leaves
88+
const subdigestTree = createSubdigestTree(subdigestLeaves)
89+
90+
return {
91+
threshold: 1n,
92+
checkpoint: 0n,
93+
topology: [mainSignerLeaf, subdigestTree] as Config.Topology,
94+
}
95+
}
96+
97+
function createSubdigestTree(leaves: Config.AnyAddressSubdigestLeaf[]): Config.Topology {
98+
if (leaves.length === 0) {
99+
throw new Error('Cannot create a tree from empty leaves')
100+
}
101+
102+
if (leaves.length === 1) {
103+
return leaves[0]!
104+
}
105+
106+
const mid = Math.floor(leaves.length / 2)
107+
const left = createSubdigestTree(leaves.slice(0, mid))
108+
const right = createSubdigestTree(leaves.slice(mid))
109+
110+
return [left, right]
111+
}

packages/wallet/core/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
export * as Envelope from './envelope'
12
export * from './wallet'
23

4+
export * as AnyPay from './anypay'
5+
export * as Preconditions from './preconditions'
6+
export * as Relayer from './relayer'
37
export * as Signers from './signers'
48
export * as State from './state'
5-
export * as Relayer from './relayer'
6-
export * as Envelope from './envelope'
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { Address } from 'ox'
2+
import {
3+
Precondition,
4+
NativeBalancePrecondition,
5+
Erc20BalancePrecondition,
6+
Erc20ApprovalPrecondition,
7+
Erc721OwnershipPrecondition,
8+
Erc721ApprovalPrecondition,
9+
Erc1155BalancePrecondition,
10+
Erc1155ApprovalPrecondition,
11+
} from './types'
12+
13+
export interface IntentPrecondition {
14+
type: string
15+
data: string
16+
}
17+
18+
export function decodePreconditions(preconditions: IntentPrecondition[]): Precondition[] {
19+
const decodedPreconditions: Precondition[] = []
20+
21+
for (const p of preconditions) {
22+
const decoded = decodePrecondition(p)
23+
if (decoded) {
24+
decodedPreconditions.push(decoded)
25+
}
26+
}
27+
28+
return decodedPreconditions
29+
}
30+
31+
export function decodePrecondition(p: IntentPrecondition): Precondition | undefined {
32+
if (!p) {
33+
return undefined
34+
}
35+
36+
let precondition: Precondition | undefined
37+
38+
try {
39+
const data = JSON.parse(p.data)
40+
41+
switch (p.type) {
42+
case 'native-balance':
43+
precondition = new NativeBalancePrecondition(
44+
Address.from(data.address),
45+
data.min ? BigInt(data.min) : undefined,
46+
data.max ? BigInt(data.max) : undefined,
47+
)
48+
break
49+
50+
case 'erc20-balance':
51+
precondition = new Erc20BalancePrecondition(
52+
Address.from(data.address),
53+
Address.from(data.token),
54+
data.min ? BigInt(data.min) : undefined,
55+
data.max ? BigInt(data.max) : undefined,
56+
)
57+
break
58+
59+
case 'erc20-approval':
60+
precondition = new Erc20ApprovalPrecondition(
61+
Address.from(data.address),
62+
Address.from(data.token),
63+
Address.from(data.operator),
64+
BigInt(data.min),
65+
)
66+
break
67+
68+
case 'erc721-ownership':
69+
precondition = new Erc721OwnershipPrecondition(
70+
Address.from(data.address),
71+
Address.from(data.token),
72+
BigInt(data.tokenId),
73+
data.owned,
74+
)
75+
break
76+
77+
case 'erc721-approval':
78+
precondition = new Erc721ApprovalPrecondition(
79+
Address.from(data.address),
80+
Address.from(data.token),
81+
BigInt(data.tokenId),
82+
Address.from(data.operator),
83+
)
84+
break
85+
86+
case 'erc1155-balance':
87+
precondition = new Erc1155BalancePrecondition(
88+
Address.from(data.address),
89+
Address.from(data.token),
90+
BigInt(data.tokenId),
91+
data.min ? BigInt(data.min) : undefined,
92+
data.max ? BigInt(data.max) : undefined,
93+
)
94+
break
95+
96+
case 'erc1155-approval':
97+
precondition = new Erc1155ApprovalPrecondition(
98+
Address.from(data.address),
99+
Address.from(data.token),
100+
BigInt(data.tokenId),
101+
Address.from(data.operator),
102+
BigInt(data.min),
103+
)
104+
break
105+
106+
default:
107+
return undefined
108+
}
109+
110+
const error = precondition.isValid()
111+
if (error) {
112+
console.warn(`Invalid precondition: ${error.message}`)
113+
return undefined
114+
}
115+
116+
return precondition
117+
} catch (e) {
118+
console.warn(`Failed to decode precondition: ${e}`)
119+
return undefined
120+
}
121+
}
122+
123+
export function encodePrecondition(p: Precondition): string {
124+
const data: any = {}
125+
126+
switch (p.type()) {
127+
case 'native-balance': {
128+
const native = p as NativeBalancePrecondition
129+
data.address = native.address.toString()
130+
if (native.min !== undefined) data.min = native.min.toString()
131+
if (native.max !== undefined) data.max = native.max.toString()
132+
break
133+
}
134+
135+
case 'erc20-balance': {
136+
const erc20 = p as Erc20BalancePrecondition
137+
data.address = erc20.address.toString()
138+
data.token = erc20.token.toString()
139+
if (erc20.min !== undefined) data.min = erc20.min.toString()
140+
if (erc20.max !== undefined) data.max = erc20.max.toString()
141+
break
142+
}
143+
144+
case 'erc20-approval': {
145+
const erc20 = p as Erc20ApprovalPrecondition
146+
data.address = erc20.address.toString()
147+
data.token = erc20.token.toString()
148+
data.operator = erc20.operator.toString()
149+
data.min = erc20.min.toString()
150+
break
151+
}
152+
153+
case 'erc721-ownership': {
154+
const erc721 = p as Erc721OwnershipPrecondition
155+
data.address = erc721.address.toString()
156+
data.token = erc721.token.toString()
157+
data.tokenId = erc721.tokenId.toString()
158+
if (erc721.owned !== undefined) data.owned = erc721.owned
159+
break
160+
}
161+
162+
case 'erc721-approval': {
163+
const erc721 = p as Erc721ApprovalPrecondition
164+
data.address = erc721.address.toString()
165+
data.token = erc721.token.toString()
166+
data.tokenId = erc721.tokenId.toString()
167+
data.operator = erc721.operator.toString()
168+
break
169+
}
170+
171+
case 'erc1155-balance': {
172+
const erc1155 = p as Erc1155BalancePrecondition
173+
data.address = erc1155.address.toString()
174+
data.token = erc1155.token.toString()
175+
data.tokenId = erc1155.tokenId.toString()
176+
if (erc1155.min !== undefined) data.min = erc1155.min.toString()
177+
if (erc1155.max !== undefined) data.max = erc1155.max.toString()
178+
break
179+
}
180+
181+
case 'erc1155-approval': {
182+
const erc1155 = p as Erc1155ApprovalPrecondition
183+
data.address = erc1155.address.toString()
184+
data.token = erc1155.token.toString()
185+
data.tokenId = erc1155.tokenId.toString()
186+
data.operator = erc1155.operator.toString()
187+
data.min = erc1155.min.toString()
188+
break
189+
}
190+
}
191+
192+
return JSON.stringify(data)
193+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './types'
2+
export * from './codec'
3+
export * from './selectors'
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Precondition, NativeBalancePrecondition, Erc20BalancePrecondition } from './types'
2+
import { IntentPrecondition, decodePreconditions } from './codec'
3+
4+
export function extractChainID(precondition: IntentPrecondition): bigint | undefined {
5+
if (!precondition) {
6+
return undefined
7+
}
8+
9+
try {
10+
const data = JSON.parse(precondition.data)
11+
return data.chainID ? BigInt(data.chainID) : undefined
12+
} catch (e) {
13+
return undefined
14+
}
15+
}
16+
17+
export function extractSupportedPreconditions(preconditions: IntentPrecondition[]): Precondition[] {
18+
if (!preconditions || preconditions.length === 0) {
19+
return []
20+
}
21+
22+
return decodePreconditions(preconditions)
23+
}
24+
25+
export function extractNativeBalancePreconditions(preconditions: IntentPrecondition[]): NativeBalancePrecondition[] {
26+
if (!preconditions || preconditions.length === 0) {
27+
return []
28+
}
29+
30+
const decoded = decodePreconditions(preconditions)
31+
return decoded.filter((p): p is NativeBalancePrecondition => p.type() === 'native-balance')
32+
}
33+
34+
export function extractERC20BalancePreconditions(preconditions: IntentPrecondition[]): Erc20BalancePrecondition[] {
35+
if (!preconditions || preconditions.length === 0) {
36+
return []
37+
}
38+
39+
const decoded = decodePreconditions(preconditions)
40+
return decoded.filter((p): p is Erc20BalancePrecondition => p.type() === 'erc20-balance')
41+
}

0 commit comments

Comments
 (0)