ox 0.14.26 → 0.14.28
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/CHANGELOG.md +12 -0
- package/_cjs/tempo/MultisigConfig.js +127 -0
- package/_cjs/tempo/MultisigConfig.js.map +1 -0
- package/_cjs/tempo/ReceivePolicyReceipt.js +80 -0
- package/_cjs/tempo/ReceivePolicyReceipt.js.map +1 -0
- package/_cjs/tempo/SignatureEnvelope.js +107 -6
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/tempo/index.js +3 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/MultisigConfig.js +312 -0
- package/_esm/tempo/MultisigConfig.js.map +1 -0
- package/_esm/tempo/ReceivePolicyReceipt.js +176 -0
- package/_esm/tempo/ReceivePolicyReceipt.js.map +1 -0
- package/_esm/tempo/SignatureEnvelope.js +170 -6
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/index.js +48 -0
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/tempo/MultisigConfig.d.ts +270 -0
- package/_types/tempo/MultisigConfig.d.ts.map +1 -0
- package/_types/tempo/ReceivePolicyReceipt.d.ts +168 -0
- package/_types/tempo/ReceivePolicyReceipt.d.ts.map +1 -0
- package/_types/tempo/SignatureEnvelope.d.ts +106 -6
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +48 -0
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/package.json +11 -1
- package/tempo/MultisigConfig/package.json +6 -0
- package/tempo/MultisigConfig.test.ts +227 -0
- package/tempo/MultisigConfig.ts +423 -0
- package/tempo/ReceivePolicyReceipt/package.json +6 -0
- package/tempo/ReceivePolicyReceipt.test.ts +198 -0
- package/tempo/ReceivePolicyReceipt.ts +263 -0
- package/tempo/SignatureEnvelope.test.ts +213 -2
- package/tempo/SignatureEnvelope.ts +257 -9
- package/tempo/e2e.test.ts +217 -0
- package/tempo/index.ts +48 -0
- package/version.ts +1 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import * as AbiEvent from '../core/AbiEvent.js'
|
|
2
|
+
import * as AbiParameters from '../core/AbiParameters.js'
|
|
3
|
+
import type * as Address from '../core/Address.js'
|
|
4
|
+
import type * as Errors from '../core/Errors.js'
|
|
5
|
+
import type * as Hex from '../core/Hex.js'
|
|
6
|
+
import type { Compute } from '../core/internal/types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A TIP-1028 receive-policy claim receipt: the ABI-encoded `ClaimReceiptV1`
|
|
10
|
+
* witness emitted when an inbound transfer or mint violates the recipient's
|
|
11
|
+
* receive policy.
|
|
12
|
+
*
|
|
13
|
+
* This is the canonical, on-chain representation – the value passed to the
|
|
14
|
+
* `ReceivePolicyGuard`'s `claim` and `burn` functions. Use `decode` to read its
|
|
15
|
+
* fields.
|
|
16
|
+
*/
|
|
17
|
+
export type ReceivePolicyReceipt = Hex.Hex
|
|
18
|
+
|
|
19
|
+
/** Reason an inbound transfer or mint was blocked by a receive policy. */
|
|
20
|
+
export type BlockedReason = 'none' | 'tokenFilter' | 'receivePolicy'
|
|
21
|
+
|
|
22
|
+
/** Kind of inbound operation that was blocked. */
|
|
23
|
+
export type Kind = 'transfer' | 'mint'
|
|
24
|
+
|
|
25
|
+
/** A decoded {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt}. */
|
|
26
|
+
export type Decoded = Compute<{
|
|
27
|
+
/** Receipt layout version. */
|
|
28
|
+
version: number
|
|
29
|
+
/** TIP-20 token holding the blocked funds. */
|
|
30
|
+
token: Address.Address
|
|
31
|
+
/** Recovery authority captured when the operation was blocked. */
|
|
32
|
+
recoveryAuthority: Address.Address
|
|
33
|
+
/** Original sender (transfer) or issuer (mint). */
|
|
34
|
+
originator: Address.Address
|
|
35
|
+
/** Addressed recipient (may be a virtual address). */
|
|
36
|
+
recipient: Address.Address
|
|
37
|
+
/** Block timestamp when the operation was blocked. */
|
|
38
|
+
blockedAt: bigint
|
|
39
|
+
/** Guard nonce assigned when the operation was blocked. */
|
|
40
|
+
blockedNonce: bigint
|
|
41
|
+
/** Reason the operation was blocked. */
|
|
42
|
+
blockedReason: BlockedReason
|
|
43
|
+
/** Whether the blocked operation was a transfer or mint. */
|
|
44
|
+
kind: Kind
|
|
45
|
+
/** Application memo. */
|
|
46
|
+
memo: Hex.Hex
|
|
47
|
+
}>
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
const blockedReasons = ['none', 'tokenFilter', 'receivePolicy'] as const
|
|
51
|
+
|
|
52
|
+
/** @internal */
|
|
53
|
+
const kinds = ['transfer', 'mint'] as const
|
|
54
|
+
|
|
55
|
+
/** @internal ABI parameters for the `ClaimReceiptV1` witness. */
|
|
56
|
+
const parameters = [
|
|
57
|
+
{
|
|
58
|
+
type: 'tuple',
|
|
59
|
+
components: [
|
|
60
|
+
{ name: 'version', type: 'uint8' },
|
|
61
|
+
{ name: 'token', type: 'address' },
|
|
62
|
+
{ name: 'recoveryAuthority', type: 'address' },
|
|
63
|
+
{ name: 'originator', type: 'address' },
|
|
64
|
+
{ name: 'recipient', type: 'address' },
|
|
65
|
+
{ name: 'blockedAt', type: 'uint64' },
|
|
66
|
+
{ name: 'blockedNonce', type: 'uint64' },
|
|
67
|
+
{ name: 'blockedReason', type: 'uint8' },
|
|
68
|
+
{ name: 'kind', type: 'uint8' },
|
|
69
|
+
{ name: 'memo', type: 'bytes32' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
] as const
|
|
73
|
+
|
|
74
|
+
/** @internal `TransferBlocked` event emitted by the `ReceivePolicyGuard`. */
|
|
75
|
+
const transferBlocked = AbiEvent.from(
|
|
76
|
+
'event TransferBlocked(address indexed token, address indexed receiver, uint64 indexed blockedNonce, uint256 amount, uint8 receiptVersion, bytes receipt)',
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decodes a {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} (ABI-encoded
|
|
81
|
+
* `ClaimReceiptV1` witness) into its fields.
|
|
82
|
+
*
|
|
83
|
+
* [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts twoslash
|
|
87
|
+
* import { ReceivePolicyReceipt } from 'ox/tempo'
|
|
88
|
+
*
|
|
89
|
+
* const decoded = ReceivePolicyReceipt.decode('0x...')
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* @param receipt - The receive-policy receipt.
|
|
93
|
+
* @returns The decoded fields.
|
|
94
|
+
*/
|
|
95
|
+
export function decode(receipt: ReceivePolicyReceipt): Decoded {
|
|
96
|
+
const [decoded] = AbiParameters.decode(parameters, receipt)
|
|
97
|
+
return {
|
|
98
|
+
version: decoded.version,
|
|
99
|
+
token: decoded.token,
|
|
100
|
+
recoveryAuthority: decoded.recoveryAuthority,
|
|
101
|
+
originator: decoded.originator,
|
|
102
|
+
recipient: decoded.recipient,
|
|
103
|
+
blockedAt: decoded.blockedAt,
|
|
104
|
+
blockedNonce: decoded.blockedNonce,
|
|
105
|
+
blockedReason: blockedReasons[decoded.blockedReason] ?? 'none',
|
|
106
|
+
kind: kinds[decoded.kind] ?? 'transfer',
|
|
107
|
+
memo: decoded.memo,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export declare namespace decode {
|
|
112
|
+
type ErrorType = AbiParameters.decode.ErrorType | Errors.GlobalErrorType
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Encodes decoded fields into a
|
|
117
|
+
* {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt}. Inverse of `decode`.
|
|
118
|
+
*
|
|
119
|
+
* [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts twoslash
|
|
123
|
+
* // @noErrors
|
|
124
|
+
* import { ReceivePolicyReceipt } from 'ox/tempo'
|
|
125
|
+
*
|
|
126
|
+
* const decoded = ReceivePolicyReceipt.decode('0x...')
|
|
127
|
+
* const receipt = ReceivePolicyReceipt.encode(decoded)
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @param decoded - The decoded fields.
|
|
131
|
+
* @returns The receive-policy receipt.
|
|
132
|
+
*/
|
|
133
|
+
export function encode(decoded: Decoded): ReceivePolicyReceipt {
|
|
134
|
+
return AbiParameters.encode(parameters, [
|
|
135
|
+
{
|
|
136
|
+
version: decoded.version,
|
|
137
|
+
token: decoded.token,
|
|
138
|
+
recoveryAuthority: decoded.recoveryAuthority,
|
|
139
|
+
originator: decoded.originator,
|
|
140
|
+
recipient: decoded.recipient,
|
|
141
|
+
blockedAt: decoded.blockedAt,
|
|
142
|
+
blockedNonce: decoded.blockedNonce,
|
|
143
|
+
blockedReason: blockedReasons.indexOf(decoded.blockedReason),
|
|
144
|
+
kind: kinds.indexOf(decoded.kind),
|
|
145
|
+
memo: decoded.memo,
|
|
146
|
+
},
|
|
147
|
+
])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export declare namespace encode {
|
|
151
|
+
type ErrorType = AbiParameters.encode.ErrorType | Errors.GlobalErrorType
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Normalizes a {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} from either
|
|
156
|
+
* an encoded receipt (passthrough) or decoded fields.
|
|
157
|
+
*
|
|
158
|
+
* [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* ```ts twoslash
|
|
162
|
+
* // @noErrors
|
|
163
|
+
* import { ReceivePolicyReceipt } from 'ox/tempo'
|
|
164
|
+
*
|
|
165
|
+
* // From an encoded receipt (passthrough).
|
|
166
|
+
* const a = ReceivePolicyReceipt.from('0x...')
|
|
167
|
+
*
|
|
168
|
+
* // From decoded fields.
|
|
169
|
+
* const b = ReceivePolicyReceipt.from(ReceivePolicyReceipt.decode('0x...'))
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* @param value - An encoded receipt or decoded fields.
|
|
173
|
+
* @returns The receive-policy receipt.
|
|
174
|
+
*/
|
|
175
|
+
export function from(
|
|
176
|
+
value: ReceivePolicyReceipt | Decoded,
|
|
177
|
+
): ReceivePolicyReceipt {
|
|
178
|
+
if (typeof value === 'string') return value
|
|
179
|
+
return encode(value)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export declare namespace from {
|
|
183
|
+
type ErrorType = encode.ErrorType | Errors.GlobalErrorType
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Extracts the {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} from a
|
|
188
|
+
* `ReceivePolicyGuard` `TransferBlocked` log.
|
|
189
|
+
*
|
|
190
|
+
* Throws if the log is not a `TransferBlocked` event. Use
|
|
191
|
+
* `fromTransactionReceipt` to extract every blocked transfer in a transaction
|
|
192
|
+
* (which skips unrelated logs).
|
|
193
|
+
*
|
|
194
|
+
* [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```ts twoslash
|
|
198
|
+
* // @noErrors
|
|
199
|
+
* import { ReceivePolicyReceipt } from 'ox/tempo'
|
|
200
|
+
*
|
|
201
|
+
* const receipt = ReceivePolicyReceipt.fromLog(log)
|
|
202
|
+
* ```
|
|
203
|
+
*
|
|
204
|
+
* @param log - A `TransferBlocked` log (`data` & `topics`).
|
|
205
|
+
* @returns The receive-policy receipt.
|
|
206
|
+
*/
|
|
207
|
+
export function fromLog(log: fromLog.Log): ReceivePolicyReceipt {
|
|
208
|
+
const { receipt } = AbiEvent.decode(transferBlocked, log)
|
|
209
|
+
return receipt
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export declare namespace fromLog {
|
|
213
|
+
type Log = AbiEvent.decode.Log
|
|
214
|
+
|
|
215
|
+
type ErrorType = AbiEvent.decode.ErrorType | Errors.GlobalErrorType
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Extracts every {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} from a
|
|
220
|
+
* transaction receipt's logs.
|
|
221
|
+
*
|
|
222
|
+
* A single transaction may block multiple inbound transfers (e.g. a batched
|
|
223
|
+
* transfer to several recipients), so this returns an array – one entry per
|
|
224
|
+
* `TransferBlocked` log, in log order. Returns an empty array when no transfers
|
|
225
|
+
* were blocked.
|
|
226
|
+
*
|
|
227
|
+
* [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```ts twoslash
|
|
231
|
+
* // @noErrors
|
|
232
|
+
* import { ReceivePolicyReceipt } from 'ox/tempo'
|
|
233
|
+
*
|
|
234
|
+
* const receipts = ReceivePolicyReceipt.fromTransactionReceipt(receipt)
|
|
235
|
+
* // @log: ['0x...'] (pass each to `claim` / `burn`)
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* @param receipt - The transaction receipt (or any object with `logs`).
|
|
239
|
+
* @returns The receive-policy receipts, one per blocked transfer.
|
|
240
|
+
*/
|
|
241
|
+
export function fromTransactionReceipt(
|
|
242
|
+
receipt: fromTransactionReceipt.Receipt,
|
|
243
|
+
): readonly ReceivePolicyReceipt[] {
|
|
244
|
+
const selector = AbiEvent.getSelector(transferBlocked)
|
|
245
|
+
const receipts: ReceivePolicyReceipt[] = []
|
|
246
|
+
for (const log of receipt.logs ?? []) {
|
|
247
|
+
if (log.topics[0] !== selector) continue
|
|
248
|
+
receipts.push(fromLog(log))
|
|
249
|
+
}
|
|
250
|
+
return receipts
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export declare namespace fromTransactionReceipt {
|
|
254
|
+
type Receipt = {
|
|
255
|
+
/** Logs emitted by the transaction. */
|
|
256
|
+
logs?: readonly AbiEvent.decode.Log[] | undefined
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
type ErrorType =
|
|
260
|
+
| AbiEvent.getSelector.ErrorType
|
|
261
|
+
| fromLog.ErrorType
|
|
262
|
+
| Errors.GlobalErrorType
|
|
263
|
+
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
WebCryptoP256,
|
|
10
10
|
} from 'ox'
|
|
11
11
|
import { describe, expect, test } from 'vitest'
|
|
12
|
+
import * as MultisigConfig from './MultisigConfig.js'
|
|
12
13
|
import * as SignatureEnvelope from './SignatureEnvelope.js'
|
|
13
14
|
|
|
14
15
|
const publicKey = PublicKey.from({
|
|
@@ -512,7 +513,7 @@ describe('deserialize', () => {
|
|
|
512
513
|
SignatureEnvelope.deserialize('0xdeadbeef'),
|
|
513
514
|
).toThrowErrorMatchingInlineSnapshot(
|
|
514
515
|
`
|
|
515
|
-
[SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xde. Expected 0x01 (P256), 0x02 (WebAuthn), 0x03 (Keychain V1),
|
|
516
|
+
[SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xde. Expected 0x01 (P256), 0x02 (WebAuthn), 0x03 (Keychain V1), 0x04 (Keychain V2), or 0x05 (Multisig)
|
|
516
517
|
|
|
517
518
|
Serialized: 0xdeadbeef]
|
|
518
519
|
`,
|
|
@@ -672,7 +673,7 @@ describe('deserialize', () => {
|
|
|
672
673
|
SignatureEnvelope.deserialize(unknownType),
|
|
673
674
|
).toThrowErrorMatchingInlineSnapshot(
|
|
674
675
|
`
|
|
675
|
-
[SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xff. Expected 0x01 (P256), 0x02 (WebAuthn), 0x03 (Keychain V1),
|
|
676
|
+
[SignatureEnvelope.InvalidSerializedError: Unable to deserialize signature envelope: Unknown signature type identifier: 0xff. Expected 0x01 (P256), 0x02 (WebAuthn), 0x03 (Keychain V1), 0x04 (Keychain V2), or 0x05 (Multisig)
|
|
676
677
|
|
|
677
678
|
Serialized: 0xff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000]
|
|
678
679
|
`,
|
|
@@ -1696,6 +1697,67 @@ describe('serialize', () => {
|
|
|
1696
1697
|
})
|
|
1697
1698
|
})
|
|
1698
1699
|
|
|
1700
|
+
describe('sortMultisigApprovals', () => {
|
|
1701
|
+
const account = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const
|
|
1702
|
+
const configId = `0x${'11'.repeat(32)}` as const
|
|
1703
|
+
const payload = `0x${'42'.repeat(32)}` as const
|
|
1704
|
+
const digest = MultisigConfig.getSignPayload({
|
|
1705
|
+
account,
|
|
1706
|
+
configId,
|
|
1707
|
+
payload,
|
|
1708
|
+
})
|
|
1709
|
+
|
|
1710
|
+
const owners = Array.from({ length: 3 }, () => {
|
|
1711
|
+
const privateKey = Secp256k1.randomPrivateKey()
|
|
1712
|
+
const address = Address.fromPublicKey(
|
|
1713
|
+
Secp256k1.getPublicKey({ privateKey }),
|
|
1714
|
+
)
|
|
1715
|
+
const signature = SignatureEnvelope.from(
|
|
1716
|
+
Secp256k1.sign({ payload: digest, privateKey }),
|
|
1717
|
+
)
|
|
1718
|
+
return { address, signature } as const
|
|
1719
|
+
})
|
|
1720
|
+
const ascending = [...owners].sort((a, b) =>
|
|
1721
|
+
Hex.toBigInt(a.address) < Hex.toBigInt(b.address) ? -1 : 1,
|
|
1722
|
+
)
|
|
1723
|
+
|
|
1724
|
+
test('behavior: orders approvals ascending by recovered owner address', () => {
|
|
1725
|
+
const ordered = SignatureEnvelope.sortMultisigApprovals({
|
|
1726
|
+
account,
|
|
1727
|
+
configId,
|
|
1728
|
+
payload,
|
|
1729
|
+
// Provide approvals in reverse of the canonical order.
|
|
1730
|
+
signatures: [...ascending].reverse().map((owner) => owner.signature),
|
|
1731
|
+
})
|
|
1732
|
+
expect(ordered).toEqual(ascending.map((owner) => owner.signature))
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
test('behavior: already-sorted input is unchanged', () => {
|
|
1736
|
+
const signatures = ascending.map((owner) => owner.signature)
|
|
1737
|
+
expect(
|
|
1738
|
+
SignatureEnvelope.sortMultisigApprovals({
|
|
1739
|
+
account,
|
|
1740
|
+
configId,
|
|
1741
|
+
payload,
|
|
1742
|
+
signatures,
|
|
1743
|
+
}),
|
|
1744
|
+
).toEqual(signatures)
|
|
1745
|
+
})
|
|
1746
|
+
|
|
1747
|
+
test('behavior: recovered order matches the config owner order', () => {
|
|
1748
|
+
const ordered = SignatureEnvelope.sortMultisigApprovals({
|
|
1749
|
+
account,
|
|
1750
|
+
configId,
|
|
1751
|
+
payload,
|
|
1752
|
+
signatures: owners.map((owner) => owner.signature),
|
|
1753
|
+
})
|
|
1754
|
+
const recovered = ordered.map((signature) =>
|
|
1755
|
+
SignatureEnvelope.extractAddress({ payload: digest, signature }),
|
|
1756
|
+
)
|
|
1757
|
+
expect(recovered).toEqual(ascending.map((owner) => owner.address))
|
|
1758
|
+
})
|
|
1759
|
+
})
|
|
1760
|
+
|
|
1699
1761
|
describe('validate', () => {
|
|
1700
1762
|
describe('secp256k1', () => {
|
|
1701
1763
|
test('behavior: returns true for valid signature', () => {
|
|
@@ -2692,3 +2754,152 @@ describe('CoercionError', () => {
|
|
|
2692
2754
|
)
|
|
2693
2755
|
})
|
|
2694
2756
|
})
|
|
2757
|
+
|
|
2758
|
+
describe('multisig', () => {
|
|
2759
|
+
const account = '0x8ba6d26ff5c4e82ba0c8caf8c8ca794e1489a7ae'
|
|
2760
|
+
const configId =
|
|
2761
|
+
'0x01781fe551182476f2422c759e82d81c92e3263737afbbad57def6e8b69d21f5'
|
|
2762
|
+
|
|
2763
|
+
// P256 signatures do not carry `yParity` in the wire format, so use a clean
|
|
2764
|
+
// inner signature for round-trip equality checks.
|
|
2765
|
+
const innerP256 = SignatureEnvelope.from({
|
|
2766
|
+
signature: { r: p256Signature.r, s: p256Signature.s },
|
|
2767
|
+
publicKey,
|
|
2768
|
+
prehash: true,
|
|
2769
|
+
})
|
|
2770
|
+
|
|
2771
|
+
const envelope = SignatureEnvelope.from({
|
|
2772
|
+
type: 'multisig',
|
|
2773
|
+
account,
|
|
2774
|
+
configId,
|
|
2775
|
+
signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
|
|
2776
|
+
})
|
|
2777
|
+
|
|
2778
|
+
test('serialize: type byte 0x05 prefix', () => {
|
|
2779
|
+
const serialized = SignatureEnvelope.serialize(envelope)
|
|
2780
|
+
expect(serialized.startsWith('0x05')).toBe(true)
|
|
2781
|
+
})
|
|
2782
|
+
|
|
2783
|
+
test('serialize/deserialize round-trip', () => {
|
|
2784
|
+
const serialized = SignatureEnvelope.serialize(envelope)
|
|
2785
|
+
expect(SignatureEnvelope.deserialize(serialized)).toEqual(envelope)
|
|
2786
|
+
})
|
|
2787
|
+
|
|
2788
|
+
test('getType', () => {
|
|
2789
|
+
expect(SignatureEnvelope.getType(envelope)).toBe('multisig')
|
|
2790
|
+
})
|
|
2791
|
+
|
|
2792
|
+
test('extractAddress returns the multisig account', () => {
|
|
2793
|
+
expect(
|
|
2794
|
+
SignatureEnvelope.extractAddress({
|
|
2795
|
+
payload: '0xdeadbeef',
|
|
2796
|
+
signature: envelope,
|
|
2797
|
+
}),
|
|
2798
|
+
).toBe(account)
|
|
2799
|
+
})
|
|
2800
|
+
|
|
2801
|
+
test('toRpc/fromRpc round-trip', () => {
|
|
2802
|
+
const rpc = SignatureEnvelope.toRpc(envelope)
|
|
2803
|
+
expect(rpc.type).toBe('multisig')
|
|
2804
|
+
expect(SignatureEnvelope.fromRpc(rpc)).toEqual(envelope)
|
|
2805
|
+
})
|
|
2806
|
+
|
|
2807
|
+
test('assert: missing properties', () => {
|
|
2808
|
+
expect(() =>
|
|
2809
|
+
SignatureEnvelope.assert({ type: 'multisig', account } as never),
|
|
2810
|
+
).toThrowError()
|
|
2811
|
+
})
|
|
2812
|
+
|
|
2813
|
+
describe('init (bootstrap)', () => {
|
|
2814
|
+
const init = {
|
|
2815
|
+
salt: `0x${'00'.repeat(32)}` as const,
|
|
2816
|
+
threshold: 1,
|
|
2817
|
+
owners: [
|
|
2818
|
+
{
|
|
2819
|
+
owner: '0x1111111111111111111111111111111111111111' as const,
|
|
2820
|
+
weight: 1,
|
|
2821
|
+
},
|
|
2822
|
+
],
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
const bootstrapEnvelope = SignatureEnvelope.from({
|
|
2826
|
+
type: 'multisig',
|
|
2827
|
+
account,
|
|
2828
|
+
configId,
|
|
2829
|
+
signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
|
|
2830
|
+
init,
|
|
2831
|
+
})
|
|
2832
|
+
|
|
2833
|
+
test('serialize/deserialize round-trip with init', () => {
|
|
2834
|
+
const serialized = SignatureEnvelope.serialize(bootstrapEnvelope)
|
|
2835
|
+
expect(SignatureEnvelope.deserialize(serialized)).toEqual(
|
|
2836
|
+
bootstrapEnvelope,
|
|
2837
|
+
)
|
|
2838
|
+
})
|
|
2839
|
+
|
|
2840
|
+
test('serialize/deserialize round-trip preserves non-zero salt', () => {
|
|
2841
|
+
const salted = SignatureEnvelope.from({
|
|
2842
|
+
type: 'multisig',
|
|
2843
|
+
account,
|
|
2844
|
+
configId,
|
|
2845
|
+
signatures: [SignatureEnvelope.from(signature_secp256k1), innerP256],
|
|
2846
|
+
init: { ...init, salt: `0x${'42'.repeat(32)}` },
|
|
2847
|
+
})
|
|
2848
|
+
const serialized = SignatureEnvelope.serialize(salted)
|
|
2849
|
+
const deserialized = SignatureEnvelope.deserialize(
|
|
2850
|
+
serialized,
|
|
2851
|
+
) as SignatureEnvelope.Multisig
|
|
2852
|
+
expect(deserialized.init?.salt).toBe(`0x${'42'.repeat(32)}`)
|
|
2853
|
+
expect(deserialized).toEqual(salted)
|
|
2854
|
+
})
|
|
2855
|
+
|
|
2856
|
+
test('absent init has no `init` key after deserialize', () => {
|
|
2857
|
+
const serialized = SignatureEnvelope.serialize(envelope)
|
|
2858
|
+
const deserialized = SignatureEnvelope.deserialize(serialized)
|
|
2859
|
+
expect('init' in deserialized).toBe(false)
|
|
2860
|
+
})
|
|
2861
|
+
|
|
2862
|
+
test('init absent vs present produce different serializations', () => {
|
|
2863
|
+
expect(SignatureEnvelope.serialize(envelope)).not.toBe(
|
|
2864
|
+
SignatureEnvelope.serialize(bootstrapEnvelope),
|
|
2865
|
+
)
|
|
2866
|
+
})
|
|
2867
|
+
|
|
2868
|
+
test('toRpc/fromRpc round-trip with init', () => {
|
|
2869
|
+
const rpc = SignatureEnvelope.toRpc(bootstrapEnvelope)
|
|
2870
|
+
expect(rpc.init).toEqual(init)
|
|
2871
|
+
expect(SignatureEnvelope.fromRpc(rpc)).toEqual(bootstrapEnvelope)
|
|
2872
|
+
})
|
|
2873
|
+
|
|
2874
|
+
test('toRpc encodes owner approvals as serialized hex (node `Vec<Bytes>`)', () => {
|
|
2875
|
+
const multisig = bootstrapEnvelope as SignatureEnvelope.Multisig
|
|
2876
|
+
const rpc = SignatureEnvelope.toRpc(
|
|
2877
|
+
multisig,
|
|
2878
|
+
) as SignatureEnvelope.MultisigRpc
|
|
2879
|
+
expect(rpc.signatures).toEqual(
|
|
2880
|
+
multisig.signatures.map((s) => SignatureEnvelope.serialize(s)),
|
|
2881
|
+
)
|
|
2882
|
+
})
|
|
2883
|
+
|
|
2884
|
+
test('fromRpc detects multisig by shape (no `type` field)', () => {
|
|
2885
|
+
const rpc = SignatureEnvelope.toRpc(bootstrapEnvelope)
|
|
2886
|
+
// The node omits the `type` discriminant; detection is shape-based.
|
|
2887
|
+
const { type: _type, ...untyped } = rpc
|
|
2888
|
+
expect(SignatureEnvelope.fromRpc(untyped as never)).toEqual(
|
|
2889
|
+
bootstrapEnvelope,
|
|
2890
|
+
)
|
|
2891
|
+
})
|
|
2892
|
+
|
|
2893
|
+
test('assert: invalid init config throws', () => {
|
|
2894
|
+
expect(() =>
|
|
2895
|
+
SignatureEnvelope.assert({
|
|
2896
|
+
type: 'multisig',
|
|
2897
|
+
account,
|
|
2898
|
+
configId,
|
|
2899
|
+
signatures: [],
|
|
2900
|
+
init: { threshold: 1, owners: [] },
|
|
2901
|
+
} as never),
|
|
2902
|
+
).toThrowError()
|
|
2903
|
+
})
|
|
2904
|
+
})
|
|
2905
|
+
})
|