ox 0.14.44 → 0.14.45

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 (66) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/_cjs/tempo/KeyAuthorization.js +43 -10
  3. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  4. package/_cjs/tempo/MultisigConfig.js +33 -11
  5. package/_cjs/tempo/MultisigConfig.js.map +1 -1
  6. package/_cjs/tempo/MultisigOperation.js +18 -94
  7. package/_cjs/tempo/MultisigOperation.js.map +1 -1
  8. package/_cjs/tempo/MultisigSimulation.js +18 -46
  9. package/_cjs/tempo/MultisigSimulation.js.map +1 -1
  10. package/_cjs/tempo/SignatureEnvelope.js +72 -109
  11. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  12. package/_cjs/tempo/TransactionRequest.js +5 -1
  13. package/_cjs/tempo/TransactionRequest.js.map +1 -1
  14. package/_cjs/tempo/index.js.map +1 -1
  15. package/_cjs/version.js +1 -1
  16. package/_esm/tempo/KeyAuthorization.js +42 -9
  17. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  18. package/_esm/tempo/MultisigConfig.js +41 -22
  19. package/_esm/tempo/MultisigConfig.js.map +1 -1
  20. package/_esm/tempo/MultisigOperation.js +20 -100
  21. package/_esm/tempo/MultisigOperation.js.map +1 -1
  22. package/_esm/tempo/MultisigSimulation.js +18 -46
  23. package/_esm/tempo/MultisigSimulation.js.map +1 -1
  24. package/_esm/tempo/SignatureEnvelope.js +107 -169
  25. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  26. package/_esm/tempo/TransactionRequest.js +5 -1
  27. package/_esm/tempo/TransactionRequest.js.map +1 -1
  28. package/_esm/tempo/index.js +3 -1
  29. package/_esm/tempo/index.js.map +1 -1
  30. package/_esm/version.js +1 -1
  31. package/_types/tempo/KeyAuthorization.d.ts +48 -38
  32. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  33. package/_types/tempo/MultisigConfig.d.ts +15 -14
  34. package/_types/tempo/MultisigConfig.d.ts.map +1 -1
  35. package/_types/tempo/MultisigOperation.d.ts +3 -2
  36. package/_types/tempo/MultisigOperation.d.ts.map +1 -1
  37. package/_types/tempo/MultisigSimulation.d.ts +2 -49
  38. package/_types/tempo/MultisigSimulation.d.ts.map +1 -1
  39. package/_types/tempo/SignatureEnvelope.d.ts +69 -128
  40. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  41. package/_types/tempo/TransactionRequest.d.ts +5 -2
  42. package/_types/tempo/TransactionRequest.d.ts.map +1 -1
  43. package/_types/tempo/index.d.ts +3 -1
  44. package/_types/tempo/index.d.ts.map +1 -1
  45. package/_types/version.d.ts +1 -1
  46. package/package.json +1 -1
  47. package/tempo/AuthorizationTempo.test.ts +8 -4
  48. package/tempo/KeyAuthorization.test-d.ts +68 -33
  49. package/tempo/KeyAuthorization.test.ts +89 -8
  50. package/tempo/KeyAuthorization.ts +124 -61
  51. package/tempo/MultisigConfig.test.ts +34 -26
  52. package/tempo/MultisigConfig.ts +54 -24
  53. package/tempo/MultisigOperation.test.ts +79 -550
  54. package/tempo/MultisigOperation.ts +31 -146
  55. package/tempo/MultisigSimulation.test-d.ts +2 -2
  56. package/tempo/MultisigSimulation.test.ts +21 -110
  57. package/tempo/MultisigSimulation.ts +25 -99
  58. package/tempo/SignatureEnvelope.test-d.ts +29 -67
  59. package/tempo/SignatureEnvelope.test.ts +301 -568
  60. package/tempo/SignatureEnvelope.ts +156 -274
  61. package/tempo/TransactionRequest.test-d.ts +6 -5
  62. package/tempo/TransactionRequest.test.ts +17 -80
  63. package/tempo/TransactionRequest.ts +23 -3
  64. package/tempo/index.ts +3 -1
  65. package/tempo/multisig.e2e.test.ts +322 -861
  66. package/version.ts +1 -1
@@ -78,7 +78,89 @@ const signature_keychain = {
78
78
  userAddress: address,
79
79
  } as const satisfies SignatureEnvelope.Keychain
80
80
 
81
+ describe('multisig account binding', () => {
82
+ test('resolves Tempo account bindings before encoding', () => {
83
+ const signed = KeyAuthorization.from(
84
+ { account: `tempox${address}`, address, chainId: 1n, type: 'multisig' },
85
+ { signature: signature_secp256k1 },
86
+ )
87
+ expect(signed.account).toMatchInlineSnapshot(
88
+ `"0xBE95c3f554e9Fc85ec51bE69a3D807A0D55BCF2C"`,
89
+ )
90
+ expect(KeyAuthorization.toRpc(signed).account).toMatchInlineSnapshot(
91
+ `"0xBE95c3f554e9Fc85ec51bE69a3D807A0D55BCF2C"`,
92
+ )
93
+ expect(
94
+ KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)).account,
95
+ ).toMatchInlineSnapshot(`"0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c"`)
96
+ })
97
+
98
+ test('rejects unbound grants before signing or encoding', () => {
99
+ const authorization = { address, chainId: 1n, type: 'multisig' } as const
100
+ const signed = {
101
+ ...authorization,
102
+ signature: SignatureEnvelope.from(signature_secp256k1),
103
+ }
104
+ for (const encode of [
105
+ // @ts-expect-error Deliberately unbound multisig grant.
106
+ () => KeyAuthorization.from(authorization),
107
+ // @ts-expect-error Deliberately unbound multisig grant.
108
+ () => KeyAuthorization.toTuple(authorization),
109
+ // @ts-expect-error Deliberately unbound multisig grant.
110
+ () => KeyAuthorization.getSignPayload(authorization),
111
+ // @ts-expect-error Deliberately unbound multisig grant.
112
+ () => KeyAuthorization.serialize(authorization),
113
+ // @ts-expect-error Deliberately unbound multisig grant.
114
+ () => KeyAuthorization.toRpc(signed),
115
+ ])
116
+ expect(encode).toThrowErrorMatchingInlineSnapshot(
117
+ `[KeyAuthorization.MissingAccountError: Multisig key grants require a parent account binding.]`,
118
+ )
119
+ })
120
+
121
+ test('rejects unbound RPC and RLP grants', () => {
122
+ const signed = KeyAuthorization.from(
123
+ { account: address, address, chainId: 1n, type: 'multisig' },
124
+ { signature: signature_secp256k1 },
125
+ )
126
+ const rpc = KeyAuthorization.toRpc(signed)
127
+ for (const account of [undefined, null]) {
128
+ // @ts-expect-error Deliberately unbound multisig grant.
129
+ expect(() => KeyAuthorization.fromRpc({ ...rpc, account })).toThrowError(
130
+ KeyAuthorization.MissingAccountError,
131
+ )
132
+ }
133
+ expect(() =>
134
+ KeyAuthorization.fromTuple([['0x01', '0x03', address]]),
135
+ ).toThrowError(KeyAuthorization.MissingAccountError)
136
+ expect(KeyAuthorization.fromRpc(rpc).account).toBe(address)
137
+ expect(
138
+ KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)).account,
139
+ ).toBe(address)
140
+ })
141
+ })
142
+
81
143
  describe('from', () => {
144
+ test('accepts multisig grants and delegates', () => {
145
+ const signed = KeyAuthorization.from(
146
+ {
147
+ address,
148
+ chainId: 1n,
149
+ type: 'multisig',
150
+ account: signature_multisig.account,
151
+ },
152
+ { signature: signature_multisig },
153
+ )
154
+ expect(
155
+ KeyAuthorization.deserialize(KeyAuthorization.serialize(signed)),
156
+ ).toEqual(signed)
157
+ expect(KeyAuthorization.fromRpc(KeyAuthorization.toRpc(signed))).toEqual(
158
+ signed,
159
+ )
160
+ })
161
+ })
162
+
163
+ describe('fromRpc', () => {
82
164
  test('default', () => {
83
165
  const authorization = KeyAuthorization.from({
84
166
  address,
@@ -1154,7 +1236,6 @@ describe('deserialize', () => {
1154
1236
  "account": "0xc4a590afa7337e5cd5eb3aa60cacf91c5400044b",
1155
1237
  "address": "0x1eff47bc3a10a45d4b230b5d10e37751fe6aa718",
1156
1238
  "chainId": 4217n,
1157
- "isAdmin": false,
1158
1239
  "signature": {
1159
1240
  "account": "0xc4a590afa7337e5cd5eb3aa60cacf91c5400044b",
1160
1241
  "config": {
@@ -2589,11 +2670,11 @@ describe('admin keys (TIP-1049)', () => {
2589
2670
  expect((authTuple as unknown as unknown[]).length).toBe(3)
2590
2671
  })
2591
2672
 
2592
- test('fromTuple: drops orphan isAdmin without account', () => {
2673
+ test('fromTuple: preserves isAdmin without account', () => {
2593
2674
  const restored = KeyAuthorization.fromTuple([
2594
2675
  ['0x01', '0x', address, '0x', [], [], '0x', '0x01'],
2595
2676
  ])
2596
- expect(restored.isAdmin).toBeUndefined()
2677
+ expect(restored.isAdmin).toBe(true)
2597
2678
  expect(restored.account).toBeUndefined()
2598
2679
  })
2599
2680
 
@@ -2601,7 +2682,7 @@ describe('admin keys (TIP-1049)', () => {
2601
2682
  const restored = KeyAuthorization.fromTuple([
2602
2683
  ['0x01', '0x', address, '0x', [], [], '0x', '0x', account],
2603
2684
  ])
2604
- expect(restored.isAdmin).toBe(false)
2685
+ expect(restored.isAdmin).toBeUndefined()
2605
2686
  expect(restored.account).toBe(account)
2606
2687
  })
2607
2688
 
@@ -2647,7 +2728,7 @@ describe('admin keys (TIP-1049)', () => {
2647
2728
  })
2648
2729
  const serialized = KeyAuthorization.serialize(authorization)
2649
2730
  const restored = KeyAuthorization.deserialize(serialized)
2650
- expect(restored.isAdmin).toBe(false)
2731
+ expect(restored.isAdmin).toBeUndefined()
2651
2732
  expect(restored.account).toBe(account)
2652
2733
  })
2653
2734
 
@@ -2670,14 +2751,14 @@ describe('admin keys (TIP-1049)', () => {
2670
2751
  expect(restored.account).toBe(account)
2671
2752
  })
2672
2753
 
2673
- test('fromRpc: drops orphan isAdmin without account', () => {
2754
+ test('fromRpc: preserves isAdmin without account', () => {
2674
2755
  const authorization = KeyAuthorization.from(
2675
2756
  { address, chainId: 1n, type: 'secp256k1' },
2676
2757
  { signature: SignatureEnvelope.from(signature_secp256k1) },
2677
2758
  )
2678
2759
  const rpc = KeyAuthorization.toRpc(authorization)
2679
2760
  const restored = KeyAuthorization.fromRpc({ ...rpc, isAdmin: true })
2680
- expect(restored.isAdmin).toBeUndefined()
2761
+ expect(restored.isAdmin).toBe(true)
2681
2762
  expect(restored.account).toBeUndefined()
2682
2763
  })
2683
2764
 
@@ -2688,7 +2769,7 @@ describe('admin keys (TIP-1049)', () => {
2688
2769
  )
2689
2770
  const rpc = KeyAuthorization.toRpc(authorization)
2690
2771
  const restored = KeyAuthorization.fromRpc({ ...rpc, account })
2691
- expect(restored.isAdmin).toBe(false)
2772
+ expect(restored.isAdmin).toBeUndefined()
2692
2773
  expect(restored.account).toBe(account)
2693
2774
  })
2694
2775
 
@@ -3,7 +3,11 @@ import type * as Address from '../core/Address.js'
3
3
  import type * as Errors from '../core/Errors.js'
4
4
  import * as Hash from '../core/Hash.js'
5
5
  import * as Hex from '../core/Hex.js'
6
- import type { Compute, OneOf, UnionPartialBy } from '../core/internal/types.js'
6
+ import type {
7
+ Compute,
8
+ PartialBy,
9
+ UnionPartialBy,
10
+ } from '../core/internal/types.js'
7
11
  import * as Rlp from '../core/Rlp.js'
8
12
  import * as SignatureEnvelope from './SignatureEnvelope.js'
9
13
  import * as TempoAddress from './TempoAddress.js'
@@ -25,7 +29,7 @@ import * as TempoAddress from './TempoAddress.js'
25
29
  * - `chainId`: Chain ID for replay protection (0 = valid on any chain)
26
30
  * - `expiry`: Unix timestamp when the key expires (undefined = never expires)
27
31
  * - `limits`: Per-TIP-20 token spending limits (only applies to `transfer()` and `approve()` calls)
28
- * - `type`: Key type (`secp256k1`, `p256`, or `webAuthn`)
32
+ * - `type`: Key type (`secp256k1`, `p256`, `webAuthn`, or `multisig`)
29
33
  *
30
34
  * [Access Keys Specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#access-keys)
31
35
  */
@@ -41,6 +45,8 @@ export type KeyAuthorization<
41
45
  chainId: bigintType
42
46
  /** Unix timestamp when key expires (undefined = never expires). */
43
47
  expiry?: numberType | null | undefined
48
+ /** Whether this authorization provisions an admin access key. */
49
+ isAdmin?: boolean | undefined
44
50
  /** TIP20 spending limits for this key. */
45
51
  limits?:
46
52
  | readonly TokenLimit<bigintType, numberType, addressType>[]
@@ -53,8 +59,6 @@ export type KeyAuthorization<
53
59
  * - `[...]` = only listed contract+selector combinations allowed
54
60
  */
55
61
  scopes?: readonly Scope<addressType>[] | undefined
56
- /** Key type. (secp256k1, P256, WebAuthn). */
57
- type: SignatureEnvelope.Type
58
62
  /**
59
63
  * Optional 32-byte witness bound into the signing hash.
60
64
  *
@@ -65,32 +69,37 @@ export type KeyAuthorization<
65
69
  * [TIP-1053 Specification](https://tips.sh/1053)
66
70
  */
67
71
  witness?: Hex.Hex | undefined
68
- } & OneOf<
69
- // TIP-1049 admin access keys: `account` and `isAdmin` are paired — either
70
- // both are specified or neither. The `account` binding scopes the signing
71
- // hash to a specific account, and `isAdmin: true` provisions an admin
72
- // access key with unrestricted keychain mutator privileges.
73
- //
74
- // [TIP-1049 Specification](https://tips.sh/1049)
72
+ } & (
73
+ | {
74
+ /** Optional account address binding. */
75
+ account?: addressType | undefined
76
+ /** Primitive key type. */
77
+ type: SignatureEnvelope.Type
78
+ }
75
79
  | {
76
- /** Account address this authorization is bound to. */
80
+ /** Required parent account binding for a multisig grant. */
77
81
  account: addressType
78
- /** Whether this authorization provisions an admin access key. */
79
- isAdmin: boolean
82
+ /** Multisig key type. */
83
+ type: 'multisig'
80
84
  }
81
- | {}
82
- > &
85
+ ) &
83
86
  (signed extends true
84
- ? { signature: Signature<bigintType, numberType> }
87
+ ? {
88
+ signature:
89
+ | SignatureEnvelope.Primitive<bigintType, numberType>
90
+ | SignatureEnvelope.Multisig<bigintType, numberType>
91
+ }
85
92
  : {
86
- signature?: Signature<bigintType, numberType> | undefined
93
+ signature?:
94
+ | SignatureEnvelope.Primitive<bigintType, numberType>
95
+ | SignatureEnvelope.Multisig<bigintType, numberType>
96
+ | undefined
87
97
  })
88
98
 
89
99
  /** Signature that can authorize an access key. */
90
- export type Signature<bigintType = bigint, numberType = number> = OneOf<
100
+ export type Signature<bigintType = bigint, numberType = number> =
91
101
  | SignatureEnvelope.Primitive<bigintType, numberType>
92
102
  | SignatureEnvelope.Multisig<bigintType, numberType>
93
- >
94
103
 
95
104
  /** RPC-formatted signature that can authorize an access key. */
96
105
  export type SignatureRpc =
@@ -107,8 +116,6 @@ export type Input = KeyAuthorization<
107
116
 
108
117
  /** RPC representation matching the node's wire format. */
109
118
  export type Rpc = {
110
- /** Optional account address binding (TIP-1049). */
111
- account?: Address.Address | null | undefined
112
119
  /** Allowed call scopes (node field: `allowedCalls`). */
113
120
  allowedCalls?: readonly RpcCallScope[] | undefined
114
121
  /** Chain ID (hex quantity). */
@@ -119,33 +126,44 @@ export type Rpc = {
119
126
  isAdmin?: boolean | null | undefined
120
127
  /** Key identifier. */
121
128
  keyId: Address.Address
122
- /** Key type. */
123
- keyType: SignatureEnvelope.Type
124
129
  /** Token spending limits. */
125
130
  limits?: readonly RpcTokenLimit[] | undefined
126
131
  /** Signature authorizing this key. */
127
132
  signature: SignatureRpc
128
133
  /** Optional 32-byte witness (hex). */
129
134
  witness?: Hex.Hex | null | undefined
130
- }
135
+ } & (
136
+ | {
137
+ /** Optional account address binding (TIP-1049). */
138
+ account?: Address.Address | null | undefined
139
+ /** Primitive key type. */
140
+ keyType: SignatureEnvelope.Type
141
+ }
142
+ | {
143
+ /** Required parent account binding for a multisig grant. */
144
+ account: Address.Address
145
+ /** Multisig key type. */
146
+ keyType: 'multisig'
147
+ }
148
+ )
131
149
 
132
150
  /** RPC representation of a token limit (matches node's `TokenLimit` serde). */
133
151
  export type RpcTokenLimit = {
134
- token: Address.Address
135
152
  limit: Hex.Hex
136
153
  period?: Hex.Hex | undefined
154
+ token: Address.Address
137
155
  }
138
156
 
139
157
  /** RPC representation of a call scope (matches node's `CallScope` serde). */
140
158
  export type RpcCallScope = {
159
+ selectorRules?: readonly RpcSelectorRule[] | undefined
141
160
  target: Address.Address
142
- selectorRules?: readonly RpcSelectorRule[]
143
161
  }
144
162
 
145
163
  /** RPC representation of a selector rule (matches node's `SelectorRule` serde). */
146
164
  export type RpcSelectorRule = {
165
+ recipients?: readonly Address.Address[] | undefined
147
166
  selector: Hex.Hex
148
- recipients?: readonly Address.Address[]
149
167
  }
150
168
 
151
169
  /** Signed representation of a Key Authorization. */
@@ -156,10 +174,11 @@ export type Signed<
156
174
  > = KeyAuthorization<true, bigintType, numberType, addressType>
157
175
 
158
176
  type SignatureValue =
159
- | UnionPartialBy<Signature, 'prehash' | 'type'>
177
+ | SignatureEnvelope.from.MultisigFromConfig
178
+ | UnionPartialBy<SignatureEnvelope.Primitive, 'prehash' | 'type'>
179
+ | PartialBy<SignatureEnvelope.Multisig, 'type'>
160
180
  | SignatureEnvelope.Secp256k1Flat
161
181
  | SignatureEnvelope.Serialized
162
- | SignatureEnvelope.from.MultisigFromConfig
163
182
 
164
183
  type BaseTuple = readonly [
165
184
  chainId: Hex.Hex,
@@ -235,14 +254,6 @@ export type Tuple<signed extends boolean = boolean> = signed extends true
235
254
  export type Scope<addressType = Address.Address> = {
236
255
  /** Target contract address. */
237
256
  address: addressType
238
- /**
239
- * 4-byte function selector, or a human-readable ABI signature
240
- * (e.g. `'transfer(address,uint256)'` or `'function transfer(address,uint256)'`).
241
- *
242
- * Signatures are encoded into a 4-byte selector automatically.
243
- * Omit to allow any selector on this contract.
244
- */
245
- selector?: Hex.Hex | string | undefined
246
257
  /**
247
258
  * Recipient allowlist for this selector (first ABI `address` argument).
248
259
  *
@@ -252,6 +263,8 @@ export type Scope<addressType = Address.Address> = {
252
263
  * Only valid for constrained selectors: `transfer`, `approve`, `transferWithMemo`.
253
264
  */
254
265
  recipients?: readonly addressType[] | undefined
266
+ /** Function selector or human-readable ABI signature. */
267
+ selector?: Hex.Hex | string | undefined
255
268
  }
256
269
 
257
270
  /**
@@ -267,8 +280,6 @@ export type TokenLimit<
267
280
  numberType = number,
268
281
  addressType = Address.Address,
269
282
  > = {
270
- /** Address of the TIP-20 token. */
271
- token: addressType
272
283
  /** Maximum spending amount for this token (enforced over the key's lifetime, or per period if `period` \> 0). */
273
284
  limit: bigintType
274
285
  /**
@@ -278,6 +289,8 @@ export type TokenLimit<
278
289
  * - `\> 0` = periodic limit that resets every `period` seconds
279
290
  */
280
291
  period?: numberType | undefined
292
+ /** Address of the TIP-20 token. */
293
+ token: addressType
281
294
  }
282
295
 
283
296
  /**
@@ -424,6 +437,7 @@ export function from<
424
437
  options: from.Options<signature> = {},
425
438
  ): from.ReturnType<authorization, signature> {
426
439
  if ('keyId' in authorization) return fromRpc(authorization as Rpc) as never
440
+ assertAccountBinding(authorization.type, authorization.account)
427
441
  const auth = authorization as KeyAuthorization & {
428
442
  limits?: readonly { token: TempoAddress.Address; limit: bigint }[]
429
443
  scopes?: readonly {
@@ -436,6 +450,9 @@ export function from<
436
450
  if (auth.signature) assertSignature(auth.signature)
437
451
  const resolved = {
438
452
  ...auth,
453
+ ...(auth.account !== undefined
454
+ ? { account: TempoAddress.resolve(auth.account) }
455
+ : {}),
439
456
  address: TempoAddress.resolve(auth.address as TempoAddress.Address),
440
457
  ...(auth.limits
441
458
  ? {
@@ -479,8 +496,12 @@ export declare namespace from {
479
496
  type Options<
480
497
  signature extends SignatureValue | undefined = SignatureValue | undefined,
481
498
  > = {
482
- /** The signature to attach to the Key Authorization. */
483
- signature?: signature | Signature | undefined
499
+ /** The primitive or multisig signature to attach to the Key Authorization. */
500
+ signature?:
501
+ | signature
502
+ | SignatureEnvelope.Primitive
503
+ | SignatureEnvelope.Multisig
504
+ | undefined
484
505
  }
485
506
 
486
507
  type ReturnType<
@@ -502,7 +523,10 @@ export declare namespace from {
502
523
  >
503
524
  >
504
525
 
505
- type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
526
+ type ErrorType =
527
+ | InvalidSignatureTypeError
528
+ | MissingAccountError
529
+ | Errors.GlobalErrorType
506
530
  }
507
531
 
508
532
  /**
@@ -536,6 +560,7 @@ export function fromRpc(authorization: Rpc): Signed {
536
560
  const witness = authorization.witness ?? undefined
537
561
  const isAdmin = authorization.isAdmin ?? undefined
538
562
  const account = authorization.account ?? undefined
563
+ assertAccountBinding(keyType, account)
539
564
  const signature = SignatureEnvelope.fromRpc(authorization.signature)
540
565
  assertSignature(signature)
541
566
  if (witness !== undefined) assertWitness(witness)
@@ -557,9 +582,6 @@ export function fromRpc(authorization: Rpc): Signed {
557
582
  })
558
583
  : undefined
559
584
 
560
- const accountBinding =
561
- account !== undefined ? { account, isAdmin: isAdmin ?? false } : {}
562
-
563
585
  return {
564
586
  address: keyId,
565
587
  chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
@@ -573,14 +595,20 @@ export function fromRpc(authorization: Rpc): Signed {
573
595
  })),
574
596
  ...(scopes ? { scopes } : {}),
575
597
  signature,
576
- type: keyType,
598
+ ...(keyType === 'multisig'
599
+ ? { account: account!, type: keyType }
600
+ : { type: keyType }),
577
601
  ...(witness !== undefined ? { witness } : {}),
578
- ...accountBinding,
602
+ ...(account !== undefined ? { account } : {}),
603
+ ...(isAdmin ? { isAdmin: true as const } : {}),
579
604
  }
580
605
  }
581
606
 
582
607
  export declare namespace fromRpc {
583
- type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
608
+ type ErrorType =
609
+ | InvalidSignatureTypeError
610
+ | MissingAccountError
611
+ | Errors.GlobalErrorType
584
612
  }
585
613
 
586
614
  /**
@@ -642,6 +670,8 @@ export function fromTuple<const tuple extends Tuple>(
642
670
  return 'p256'
643
671
  case '0x02':
644
672
  return 'webAuthn'
673
+ case '0x03':
674
+ return 'multisig'
645
675
  default:
646
676
  throw new Error(`Invalid key type: ${keyType_hex}`)
647
677
  }
@@ -697,17 +727,19 @@ export function fromTuple<const tuple extends Tuple>(
697
727
  const account = isAbsent(rawAccount)
698
728
  ? undefined
699
729
  : (rawAccount as Address.Address)
700
- const accountBinding =
701
- account !== undefined ? { account, isAdmin: isAdmin ?? false } : {}
730
+ assertAccountBinding(keyType, account)
702
731
  const args: KeyAuthorization = {
703
732
  address: keyId,
704
733
  chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
705
- type: keyType,
734
+ ...(keyType === 'multisig'
735
+ ? { account: account!, type: keyType }
736
+ : { type: keyType }),
706
737
  ...(expiry !== undefined ? { expiry } : {}),
707
738
  ...(limits !== undefined ? { limits } : {}),
708
739
  ...(scopes !== undefined ? { scopes } : {}),
709
740
  ...(witness !== undefined ? { witness } : {}),
710
- ...accountBinding,
741
+ ...(account !== undefined ? { account } : {}),
742
+ ...(isAdmin ? { isAdmin: true as const } : {}),
711
743
  }
712
744
  if (signatureSerialized) {
713
745
  const signature = SignatureEnvelope.deserialize(signatureSerialized)
@@ -722,7 +754,10 @@ export declare namespace fromTuple {
722
754
  KeyAuthorization<authorization extends Tuple<true> ? true : false>
723
755
  >
724
756
 
725
- type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
757
+ type ErrorType =
758
+ | InvalidSignatureTypeError
759
+ | MissingAccountError
760
+ | Errors.GlobalErrorType
726
761
  }
727
762
 
728
763
  /**
@@ -914,6 +949,7 @@ export declare namespace serialize {
914
949
  * @returns An RPC-formatted Key Authorization.
915
950
  */
916
951
  export function toRpc(authorization: Signed): Rpc {
952
+ assertAccountBinding(authorization.type, authorization.account)
917
953
  const {
918
954
  address,
919
955
  scopes,
@@ -957,13 +993,15 @@ export function toRpc(authorization: Signed): Rpc {
957
993
  chainId: chainId === 0n ? '0x' : Hex.fromNumber(chainId),
958
994
  expiry: typeof expiry === 'number' ? Hex.fromNumber(expiry) : null,
959
995
  keyId: TempoAddress.resolve(address),
960
- keyType: type,
996
+ ...(type === 'multisig' ? { account, keyType: type } : { keyType: type }),
961
997
  limits: limits?.map(({ token, limit, period }) => ({
962
998
  token,
963
999
  limit: Hex.fromNumber(limit),
964
1000
  ...(period ? { period: numberToHex(period) } : {}),
965
1001
  })),
966
- signature: SignatureEnvelope.toRpc(signature) as SignatureRpc,
1002
+ signature: SignatureEnvelope.toRpc(signature) as
1003
+ | SignatureEnvelope.PrimitiveRpc
1004
+ | SignatureEnvelope.MultisigRpc,
967
1005
  ...(allowedCalls ? { allowedCalls } : {}),
968
1006
  ...(witness !== undefined ? { witness } : {}),
969
1007
  ...(isAdmin ? { isAdmin: true } : {}),
@@ -972,7 +1010,10 @@ export function toRpc(authorization: Signed): Rpc {
972
1010
  }
973
1011
 
974
1012
  export declare namespace toRpc {
975
- type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
1013
+ type ErrorType =
1014
+ | InvalidSignatureTypeError
1015
+ | MissingAccountError
1016
+ | Errors.GlobalErrorType
976
1017
  }
977
1018
 
978
1019
  /**
@@ -1009,6 +1050,7 @@ export declare namespace toRpc {
1009
1050
  export function toTuple<const authorization extends KeyAuthorization>(
1010
1051
  authorization: authorization,
1011
1052
  ): toTuple.ReturnType<authorization> {
1053
+ assertAccountBinding(authorization.type, authorization.account)
1012
1054
  const {
1013
1055
  address,
1014
1056
  chainId,
@@ -1025,16 +1067,19 @@ export function toTuple<const authorization extends KeyAuthorization>(
1025
1067
  assertSignature(authorization.signature)
1026
1068
  return SignatureEnvelope.serialize(authorization.signature)
1027
1069
  })()
1070
+ const keyType = authorization.type
1028
1071
  const type = (() => {
1029
- switch (authorization.type) {
1072
+ switch (keyType) {
1030
1073
  case 'secp256k1':
1031
1074
  return '0x'
1032
1075
  case 'p256':
1033
1076
  return '0x01'
1034
1077
  case 'webAuthn':
1035
1078
  return '0x02'
1079
+ case 'multisig':
1080
+ return '0x03'
1036
1081
  default:
1037
- throw new Error(`Invalid key type: ${authorization.type}`)
1082
+ throw new Error(`Invalid key type: ${keyType}`)
1038
1083
  }
1039
1084
  })()
1040
1085
  const limitsValue = limits?.map((limit) => {
@@ -1083,7 +1128,7 @@ export function toTuple<const authorization extends KeyAuthorization>(
1083
1128
  // single entry to this list with `placeholder: '0x'`.
1084
1129
  const hasTip1053Plus =
1085
1130
  witness !== undefined || isAdmin || account !== undefined
1086
- const optionals: readonly { value: unknown; placeholder: unknown }[] = [
1131
+ const optionals: readonly { placeholder: unknown; value: unknown }[] = [
1087
1132
  {
1088
1133
  value:
1089
1134
  expiry !== null && expiry !== undefined && expiry !== 0
@@ -1120,7 +1165,10 @@ export declare namespace toTuple {
1120
1165
  type ReturnType<authorization extends KeyAuthorization = KeyAuthorization> =
1121
1166
  Compute<Tuple<authorization extends KeyAuthorization<true> ? true : false>>
1122
1167
 
1123
- type ErrorType = InvalidSignatureTypeError | Errors.GlobalErrorType
1168
+ type ErrorType =
1169
+ | InvalidSignatureTypeError
1170
+ | MissingAccountError
1171
+ | Errors.GlobalErrorType
1124
1172
  }
1125
1173
 
1126
1174
  function bigintToHex(value: bigint): Hex.Hex {
@@ -1154,6 +1202,13 @@ function resolveSelector(
1154
1202
  return AbiItem.getSelector(selector)
1155
1203
  }
1156
1204
 
1205
+ function assertAccountBinding(
1206
+ type: KeyAuthorization['type'],
1207
+ account: TempoAddress.Address | null | undefined,
1208
+ ): void {
1209
+ if (type === 'multisig' && account == null) throw new MissingAccountError()
1210
+ }
1211
+
1157
1212
  function assertWitness(witness: Hex.Hex): void {
1158
1213
  if (Hex.size(witness) !== 32) throw new InvalidWitnessSizeError(witness)
1159
1214
  }
@@ -1198,3 +1253,11 @@ export class InvalidSignatureTypeError extends Error {
1198
1253
  )
1199
1254
  }
1200
1255
  }
1256
+
1257
+ /** Thrown when a multisig key grant omits its parent account binding. */
1258
+ export class MissingAccountError extends Error {
1259
+ override readonly name = 'KeyAuthorization.MissingAccountError'
1260
+ constructor() {
1261
+ super('Multisig key grants require a parent account binding.')
1262
+ }
1263
+ }