ox 0.14.29 → 0.14.31

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 (54) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/_cjs/core/AbiParameters.js +2 -1
  3. package/_cjs/core/AbiParameters.js.map +1 -1
  4. package/_cjs/core/internal/abiParameters.js +10 -2
  5. package/_cjs/core/internal/abiParameters.js.map +1 -1
  6. package/_cjs/tempo/KeyAuthorization.js +39 -10
  7. package/_cjs/tempo/KeyAuthorization.js.map +1 -1
  8. package/_cjs/tempo/MultisigConfig.js +26 -26
  9. package/_cjs/tempo/MultisigConfig.js.map +1 -1
  10. package/_cjs/tempo/SignatureEnvelope.js +189 -55
  11. package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
  12. package/_cjs/version.js +1 -1
  13. package/_esm/core/AbiParameters.js +4 -1
  14. package/_esm/core/AbiParameters.js.map +1 -1
  15. package/_esm/core/internal/abiParameters.js +17 -2
  16. package/_esm/core/internal/abiParameters.js.map +1 -1
  17. package/_esm/tempo/KeyAuthorization.js +41 -9
  18. package/_esm/tempo/KeyAuthorization.js.map +1 -1
  19. package/_esm/tempo/MultisigConfig.js +53 -81
  20. package/_esm/tempo/MultisigConfig.js.map +1 -1
  21. package/_esm/tempo/SignatureEnvelope.js +202 -75
  22. package/_esm/tempo/SignatureEnvelope.js.map +1 -1
  23. package/_esm/tempo/index.js +2 -2
  24. package/_esm/version.js +1 -1
  25. package/_types/core/AbiParameters.d.ts.map +1 -1
  26. package/_types/core/internal/abiParameters.d.ts.map +1 -1
  27. package/_types/core/internal/rpcSchemas/eth.d.ts +16 -0
  28. package/_types/core/internal/rpcSchemas/eth.d.ts.map +1 -1
  29. package/_types/tempo/KeyAuthorization.d.ts +22 -16
  30. package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
  31. package/_types/tempo/MultisigConfig.d.ts +43 -83
  32. package/_types/tempo/MultisigConfig.d.ts.map +1 -1
  33. package/_types/tempo/SignatureEnvelope.d.ts +62 -40
  34. package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
  35. package/_types/tempo/index.d.ts +2 -2
  36. package/_types/version.d.ts +1 -1
  37. package/core/AbiParameters.ts +3 -1
  38. package/core/internal/abiParameters.ts +17 -2
  39. package/core/internal/rpcSchemas/eth.ts +16 -0
  40. package/package.json +11 -1
  41. package/tempo/KeyAuthorization.test-d/package.json +6 -0
  42. package/tempo/KeyAuthorization.test-d.ts +62 -0
  43. package/tempo/KeyAuthorization.test.ts +132 -0
  44. package/tempo/KeyAuthorization.ts +76 -29
  45. package/tempo/MultisigConfig.test.ts +86 -37
  46. package/tempo/MultisigConfig.ts +75 -132
  47. package/tempo/SignatureEnvelope.test-d/package.json +6 -0
  48. package/tempo/SignatureEnvelope.test-d.ts +97 -0
  49. package/tempo/SignatureEnvelope.test.ts +322 -30
  50. package/tempo/SignatureEnvelope.ts +314 -118
  51. package/tempo/e2e.test.ts +22 -207
  52. package/tempo/index.ts +2 -2
  53. package/tempo/multisig.e2e.test.ts +582 -0
  54. package/version.ts +1 -1
@@ -72,11 +72,15 @@ export type GetType<
72
72
  userAddress: Address.Address
73
73
  }
74
74
  ? 'keychain'
75
- : envelope extends {
76
- account: Address.Address
77
- genesisConfigId: `0x${string}`
78
- signatures: any
79
- }
75
+ : envelope extends
76
+ | {
77
+ account: Address.Address
78
+ signatures: any
79
+ }
80
+ | {
81
+ init: MultisigConfig.Config
82
+ signatures: any
83
+ }
80
84
  ? 'multisig'
81
85
  : never
82
86
 
@@ -118,6 +122,16 @@ export type SignatureEnvelopeRpc = OneOf<
118
122
  Secp256k1Rpc | P256Rpc | WebAuthnRpc | KeychainRpc | MultisigRpc
119
123
  >
120
124
 
125
+ /** Primitive signature envelope accepted by protocol sidecars. */
126
+ export type Primitive<bigintType = bigint, numberType = number> = OneOf<
127
+ | Secp256k1<bigintType, numberType>
128
+ | P256<bigintType, numberType>
129
+ | WebAuthn<bigintType, numberType>
130
+ >
131
+
132
+ /** RPC-formatted primitive signature envelope. */
133
+ export type PrimitiveRpc = OneOf<Secp256k1Rpc | P256Rpc | WebAuthnRpc>
134
+
121
135
  /**
122
136
  * Keychain signature version.
123
137
  *
@@ -150,9 +164,10 @@ export type KeychainRpc = {
150
164
  /**
151
165
  * Native multisig signature (type `0x05`).
152
166
  *
153
- * Wraps a set of primitive owner approvals (secp256k1, p256, or webAuthn) over the
154
- * multisig owner approval digest. The transaction sender is the derived `account`,
155
- * authorized once the recovered owner weights meet the configured threshold.
167
+ * Wraps a set of owner approvals (secp256k1, p256, webAuthn, or nested
168
+ * multisig) over the multisig owner approval digest. The transaction sender is
169
+ * the derived `account`, authorized once the recovered owner weights meet the
170
+ * configured threshold.
156
171
  *
157
172
  * [TIP-1061](https://tips.sh/1061)
158
173
  */
@@ -160,9 +175,11 @@ export type Multisig<bigintType = bigint, numberType = number> = {
160
175
  type: 'multisig'
161
176
  /** Native multisig account address. */
162
177
  account: Address.Address
163
- /** Permanent config ID derived from the initial multisig config. */
164
- genesisConfigId: Hex.Hex
165
- /** Primitive owner approvals over the multisig owner approval digest. */
178
+ /**
179
+ * Owner approvals over the multisig owner approval digest. Each approval is
180
+ * either a primitive signature or a nested multisig signature (keychain
181
+ * approvals are invalid).
182
+ */
166
183
  signatures: readonly SignatureEnvelope<bigintType, numberType>[]
167
184
  /**
168
185
  * Initial native multisig config for bootstrapping this account. Present only on
@@ -172,22 +189,25 @@ export type Multisig<bigintType = bigint, numberType = number> = {
172
189
  init?: MultisigConfig.Config<numberType> | undefined
173
190
  }
174
191
 
175
- export type MultisigRpc = {
176
- type: 'multisig'
177
- account: Address.Address
178
- /**
179
- * The permanent multisig config ID (TIP-1061 wire field `config_id`).
180
- * Maps to `genesisConfigId` on the typed
181
- * {@link ox#SignatureEnvelope.Multisig} envelope.
182
- */
183
- configId: Hex.Hex
184
- /**
185
- * Encoded primitive owner approvals (raw serialized signatures), matching the
186
- * node's `Vec<Bytes>` representation.
187
- */
188
- signatures: readonly Serialized[]
189
- init?: MultisigConfig.Config | undefined
190
- }
192
+ /** RPC-formatted native multisig signature. */
193
+ export type MultisigRpc = OneOf<
194
+ | {
195
+ /** Existing native multisig account. */
196
+ account: Address.Address
197
+ /** Structured owner approvals. */
198
+ signatures: readonly SignatureEnvelopeRpc[]
199
+ /** Multisig RPC signatures are untagged. */
200
+ type?: undefined
201
+ }
202
+ | {
203
+ /** Initial config for bootstrapping a native multisig account. */
204
+ init: MultisigConfig.Config
205
+ /** Structured owner approvals. */
206
+ signatures: readonly SignatureEnvelopeRpc[]
207
+ /** Multisig RPC signatures are untagged. */
208
+ type?: undefined
209
+ }
210
+ >
191
211
 
192
212
  export type P256<bigintType = bigint, numberType = number> = {
193
213
  prehash: boolean
@@ -331,14 +351,7 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
331
351
 
332
352
  if (type === 'multisig') {
333
353
  const multisig = envelope as Multisig
334
- const missing: string[] = []
335
- if (!multisig.account) missing.push('account')
336
- if (!multisig.genesisConfigId) missing.push('genesisConfigId')
337
- if (!Array.isArray(multisig.signatures)) missing.push('signatures')
338
- if (missing.length > 0)
339
- throw new MissingPropertiesError({ envelope, missing, type: 'multisig' })
340
- for (const inner of multisig.signatures) assert(inner)
341
- if (multisig.init) MultisigConfig.assert(multisig.init)
354
+ assertMultisig(multisig, 1)
342
355
  return
343
356
  }
344
357
  }
@@ -346,12 +359,80 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
346
359
  export declare namespace assert {
347
360
  type ErrorType =
348
361
  | CoercionError
362
+ | InvalidMultisigApprovalError
349
363
  | MissingPropertiesError
350
364
  | MultisigConfig.assert.ErrorType
365
+ | MultisigConfig.getAddress.ErrorType
351
366
  | Signature.assert.ErrorType
352
367
  | Errors.GlobalErrorType
353
368
  }
354
369
 
370
+ function assertMultisig(envelope: Multisig, depth: number): void {
371
+ const missing: string[] = []
372
+ if (!envelope.account) missing.push('account')
373
+ if (!Array.isArray(envelope.signatures)) missing.push('signatures')
374
+ if (missing.length > 0)
375
+ throw new MissingPropertiesError({
376
+ envelope,
377
+ missing,
378
+ type: 'multisig',
379
+ })
380
+ if (depth > MultisigConfig.maxNestingDepth)
381
+ throw new InvalidMultisigApprovalError({
382
+ reason: `multisig nesting depth exceeds ${MultisigConfig.maxNestingDepth}`,
383
+ })
384
+ if (!Address.validate(envelope.account))
385
+ throw new InvalidMultisigApprovalError({
386
+ reason: 'multisig account is invalid',
387
+ })
388
+ if (Hex.toBigInt(envelope.account) === 0n)
389
+ throw new InvalidMultisigApprovalError({
390
+ reason: 'multisig account cannot be zero',
391
+ })
392
+ if (envelope.signatures.length === 0)
393
+ throw new InvalidMultisigApprovalError({
394
+ reason: 'multisig signatures cannot be empty',
395
+ })
396
+ if (envelope.signatures.length > MultisigConfig.maxSignatures)
397
+ throw new InvalidMultisigApprovalError({
398
+ reason: `multisig signatures exceed ${MultisigConfig.maxSignatures}`,
399
+ })
400
+
401
+ if (envelope.init) {
402
+ MultisigConfig.assert(envelope.init)
403
+ if (
404
+ !Address.isEqual(
405
+ MultisigConfig.getAddress(envelope.init),
406
+ envelope.account,
407
+ )
408
+ )
409
+ throw new InvalidMultisigApprovalError({
410
+ reason: 'multisig init does not derive account',
411
+ })
412
+ }
413
+
414
+ for (const inner of envelope.signatures) {
415
+ const type = getType(inner)
416
+ if (type === 'keychain')
417
+ throw new InvalidMultisigApprovalError({
418
+ reason: 'keychain owner approvals are not allowed',
419
+ })
420
+ if (type === 'multisig') {
421
+ const multisig = inner as Multisig
422
+ if (multisig.init)
423
+ throw new InvalidMultisigApprovalError({
424
+ reason: 'nested multisig owner approvals cannot carry `init`',
425
+ })
426
+ assertMultisig(multisig, depth + 1)
427
+ } else assert(inner)
428
+
429
+ if (Hex.size(serialize(inner)) > MultisigConfig.maxOwnerSignatureBytes)
430
+ throw new InvalidMultisigApprovalError({
431
+ reason: `multisig owner signature exceeds ${MultisigConfig.maxOwnerSignatureBytes} bytes`,
432
+ })
433
+ }
434
+ }
435
+
355
436
  /**
356
437
  * Extracts the address of the signer from a {@link ox#SignatureEnvelope.SignatureEnvelope}.
357
438
  *
@@ -497,6 +578,13 @@ export declare namespace extractPublicKey {
497
578
  * @throws `CoercionError` if the serialized value cannot be coerced to a valid signature envelope.
498
579
  */
499
580
  export function deserialize(value: Serialized): SignatureEnvelope {
581
+ return deserialize_(value, 0)
582
+ }
583
+
584
+ function deserialize_(
585
+ value: Serialized,
586
+ multisigDepth: number,
587
+ ): SignatureEnvelope {
500
588
  const serialized = value.endsWith(magicBytes.slice(2))
501
589
  ? Hex.slice(value, 0, -Hex.size(magicBytes))
502
590
  : value
@@ -607,7 +695,7 @@ export function deserialize(value: Serialized): SignatureEnvelope {
607
695
  typeId === serializedKeychainV2Type
608
696
  ) {
609
697
  const userAddress = Hex.slice(data, 0, 20)
610
- const inner = deserialize(Hex.slice(data, 20))
698
+ const inner = deserialize_(Hex.slice(data, 20), multisigDepth)
611
699
 
612
700
  return {
613
701
  userAddress,
@@ -618,26 +706,91 @@ export function deserialize(value: Serialized): SignatureEnvelope {
618
706
  }
619
707
 
620
708
  if (typeId === serializedMultisigType) {
621
- // Wire format: `0x05 || rlp([account, genesisConfigId, signatures, init])`. `init`
622
- // is optional: absent when the element is missing or the `0x80` placeholder
623
- // (decoded as the empty string `0x`), otherwise the bootstrap config list.
624
- const [account, genesisConfigId, signatures, init] = Rlp.toHex(data) as [
625
- Hex.Hex,
626
- Hex.Hex,
627
- readonly Hex.Hex[],
628
- (Hex.Hex | MultisigConfig.Tuple)?,
629
- ]
630
- return {
709
+ const depth = multisigDepth + 1
710
+ if (depth > MultisigConfig.maxNestingDepth)
711
+ throw new InvalidSerializedError({
712
+ reason: `multisig nesting depth exceeds ${MultisigConfig.maxNestingDepth}`,
713
+ serialized,
714
+ })
715
+
716
+ // The first field distinguishes the static wire shapes: a bootstrap init
717
+ // config is an RLP list, while an initialized account is a 20-byte string.
718
+ const decoded = Rlp.toHex(data)
719
+ if (!Array.isArray(decoded) || decoded.length !== 2)
720
+ throw new InvalidSerializedError({
721
+ reason: 'invalid multisig wire shape: expected exactly two fields',
722
+ serialized,
723
+ })
724
+
725
+ const [address, signatures] = decoded
726
+ if (!Array.isArray(signatures) || signatures.some(Array.isArray))
727
+ throw new InvalidSerializedError({
728
+ reason: 'invalid multisig signatures list',
729
+ serialized,
730
+ })
731
+ if (signatures.length === 0)
732
+ throw new InvalidSerializedError({
733
+ reason: 'multisig signatures cannot be empty',
734
+ serialized,
735
+ })
736
+ if (signatures.length > MultisigConfig.maxSignatures)
737
+ throw new InvalidSerializedError({
738
+ reason: `multisig signatures exceed ${MultisigConfig.maxSignatures}`,
739
+ serialized,
740
+ })
741
+ for (const signature of signatures)
742
+ if (
743
+ Hex.size(signature as Hex.Hex) > MultisigConfig.maxOwnerSignatureBytes
744
+ )
745
+ throw new InvalidSerializedError({
746
+ reason: `multisig owner signature exceeds ${MultisigConfig.maxOwnerSignatureBytes} bytes`,
747
+ serialized,
748
+ })
749
+
750
+ if (!Array.isArray(address) && !Address.validate(address))
751
+ throw new InvalidSerializedError({
752
+ reason: 'invalid multisig account',
753
+ serialized,
754
+ })
755
+ if (Array.isArray(address)) {
756
+ const [salt, threshold, owners] = address
757
+ if (
758
+ address.length !== 3 ||
759
+ Array.isArray(salt) ||
760
+ Hex.size(salt) !== 32 ||
761
+ Array.isArray(threshold) ||
762
+ Hex.size(threshold) > 1 ||
763
+ !Array.isArray(owners) ||
764
+ owners.some(
765
+ (owner) =>
766
+ !Array.isArray(owner) ||
767
+ owner.length !== 2 ||
768
+ owner.some(Array.isArray) ||
769
+ Hex.size(owner[1] as Hex.Hex) > 1,
770
+ )
771
+ )
772
+ throw new InvalidSerializedError({
773
+ reason: 'invalid multisig init config',
774
+ serialized,
775
+ })
776
+ }
777
+
778
+ const init = Array.isArray(address)
779
+ ? MultisigConfig.fromTuple(address as unknown as MultisigConfig.Tuple)
780
+ : undefined
781
+ const account = init
782
+ ? MultisigConfig.getAddress(init)
783
+ : (address as Address.Address)
784
+ const envelope = {
631
785
  type: 'multisig',
632
786
  account,
633
- genesisConfigId,
634
- signatures: signatures.map((signature) => deserialize(signature)),
635
- ...(init && init !== '0x'
636
- ? {
637
- init: MultisigConfig.fromTuple(init as MultisigConfig.Tuple),
638
- }
639
- : {}),
787
+ signatures: signatures.map((signature) =>
788
+ deserialize_(signature as Hex.Hex, depth),
789
+ ),
790
+ ...(init ? { init } : {}),
640
791
  } satisfies Multisig
792
+ assertMultisig(envelope, depth)
793
+ return envelope
641
794
  }
642
795
 
643
796
  throw new InvalidSerializedError({
@@ -761,9 +914,9 @@ export function deserialize(value: Serialized): SignatureEnvelope {
761
914
  * @example
762
915
  * ### Multisig (from genesis config)
763
916
  *
764
- * Pass `genesisConfig` to derive `account` and `genesisConfigId` automatically.
765
- * Set `init: true` to opt into bootstrap (uses `genesisConfig` as the
766
- * bootstrap `init`); omit `init` for subsequent (non-bootstrap) transactions.
917
+ * Pass `genesisConfig` to derive `account` automatically. Set `init: true` to
918
+ * opt into bootstrap (uses `genesisConfig` as the bootstrap `init`); omit
919
+ * `init` for subsequent (non-bootstrap) transactions.
767
920
  *
768
921
  * ```ts twoslash
769
922
  * import { Secp256k1 } from 'ox'
@@ -821,18 +974,12 @@ export function from<const value extends from.Value>(
821
974
  init?: MultisigConfig.Config | boolean | undefined
822
975
  }
823
976
  const { genesisConfig, init, ...rest } = multisig
824
- // Derive `account`/`genesisConfigId` from `genesisConfig` when not provided
825
- // explicitly.
977
+ // Derive `account` from `genesisConfig` when not provided explicitly.
826
978
  const account = (() => {
827
979
  if (rest.account) return rest.account
828
980
  if (genesisConfig) return MultisigConfig.getAddress(genesisConfig)
829
981
  return rest.account
830
982
  })()
831
- const genesisConfigId = (() => {
832
- if (rest.genesisConfigId) return rest.genesisConfigId
833
- if (genesisConfig) return MultisigConfig.toId(genesisConfig)
834
- return rest.genesisConfigId
835
- })()
836
983
  // `init: true` opts into bootstrap using the supplied `genesisConfig`.
837
984
  // Otherwise, `init` is treated as the explicit bootstrap config (or
838
985
  // omitted).
@@ -840,7 +987,6 @@ export function from<const value extends from.Value>(
840
987
  return {
841
988
  ...rest,
842
989
  account,
843
- genesisConfigId,
844
990
  signatures: rest.signatures.map((signature) => from(signature)),
845
991
  // Normalize the bootstrap config (sorts owners, defaults the salt) so the
846
992
  // in-memory envelope matches what `deserialize` reconstructs.
@@ -892,9 +1038,9 @@ export declare namespace from {
892
1038
  }
893
1039
 
894
1040
  /**
895
- * Multisig envelope input variant where `account` and `genesisConfigId` are derived
896
- * from the supplied `genesisConfig`. Pass `init: true` to opt into bootstrap
897
- * (uses `genesisConfig` as the bootstrap `init`); omit `init` for subsequent
1041
+ * Multisig envelope input variant where `account` is derived from the
1042
+ * supplied `genesisConfig`. Pass `init: true` to opt into bootstrap (uses
1043
+ * `genesisConfig` as the bootstrap `init`); omit `init` for subsequent
898
1044
  * (non-bootstrap) transactions.
899
1045
  */
900
1046
  type MultisigFromGenesisConfig = {
@@ -1034,24 +1180,28 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
1034
1180
  }
1035
1181
 
1036
1182
  if (
1037
- envelope.type === 'multisig' ||
1038
- ('account' in envelope &&
1039
- 'configId' in envelope &&
1040
- 'signatures' in envelope)
1183
+ (envelope as { type?: string | undefined }).type === 'multisig' ||
1184
+ ('signatures' in envelope && ('account' in envelope || 'init' in envelope))
1041
1185
  ) {
1042
1186
  const multisig = envelope as MultisigRpc
1043
- return {
1187
+ const hasAccount = typeof multisig.account !== 'undefined'
1188
+ const hasInit = typeof multisig.init !== 'undefined'
1189
+ if (hasAccount === hasInit)
1190
+ throw new InvalidMultisigApprovalError({
1191
+ reason: 'RPC multisig must contain exactly one of `account` or `init`',
1192
+ })
1193
+ const init = hasInit
1194
+ ? MultisigConfig.from(multisig.init as MultisigConfig.Config)
1195
+ : undefined
1196
+ const account = init ? MultisigConfig.getAddress(init) : multisig.account
1197
+ const result = {
1044
1198
  type: 'multisig',
1045
- account: multisig.account,
1046
- // Map RPC wire field `configId` (TIP-1061 spec name) to the typed
1047
- // envelope's `genesisConfigId`.
1048
- genesisConfigId: multisig.configId,
1049
- // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
1050
- signatures: multisig.signatures.map((signature) =>
1051
- deserialize(signature),
1052
- ),
1053
- ...(multisig.init ? { init: MultisigConfig.from(multisig.init) } : {}),
1054
- }
1199
+ account: account as Address.Address,
1200
+ signatures: multisig.signatures.map((signature) => fromRpc(signature)),
1201
+ ...(init ? { init } : {}),
1202
+ } satisfies Multisig
1203
+ assert(result)
1204
+ return result
1055
1205
  }
1056
1206
 
1057
1207
  throw new CoercionError({ envelope })
@@ -1059,8 +1209,10 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
1059
1209
 
1060
1210
  export declare namespace fromRpc {
1061
1211
  type ErrorType =
1212
+ | assert.ErrorType
1062
1213
  | CoercionError
1063
1214
  | InvalidSerializedError
1215
+ | MultisigConfig.getAddress.ErrorType
1064
1216
  | Signature.fromRpc.ErrorType
1065
1217
  | Errors.GlobalErrorType
1066
1218
  }
@@ -1132,8 +1284,9 @@ export function getType<
1132
1284
 
1133
1285
  // Detect Multisig signature
1134
1286
  if (
1135
- (('account' in envelope && 'genesisConfigId' in envelope) ||
1136
- 'genesisConfig' in envelope) &&
1287
+ ('account' in envelope ||
1288
+ 'genesisConfig' in envelope ||
1289
+ 'init' in envelope) &&
1137
1290
  'signatures' in envelope
1138
1291
  )
1139
1292
  return 'multisig' as never
@@ -1152,6 +1305,7 @@ export function getType<
1152
1305
  * - WebAuthn: `0x02` + webauthnData (variable) + r (32) + s (32) + pubKeyX (32) + pubKeyY (32)
1153
1306
  * - Keychain V1: `0x03` + userAddress (20) + inner signature (recursive)
1154
1307
  * - Keychain V2: `0x04` + userAddress (20) + inner signature (recursive)
1308
+ * - Multisig: `0x05` + RLP `[account | init, signatures]`
1155
1309
  *
1156
1310
  * [Signature Types](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#signature-types)
1157
1311
  *
@@ -1233,17 +1387,16 @@ export function serialize(
1233
1387
 
1234
1388
  if (type === 'multisig') {
1235
1389
  const multisig = envelope as Multisig
1236
- // Format: `0x05 || rlp([account, genesisConfigId, signatures, init])`, where each
1237
- // owner approval is an encoded primitive signature. `init` is the bootstrap
1238
- // config (an RLP list) when present, otherwise the canonical empty-string
1239
- // placeholder (`0x` → RLP `0x80`).
1390
+ assert(multisig)
1391
+ // The first field is either the initialized account or the bootstrap init
1392
+ // config. Each owner approval is an encoded signature.
1240
1393
  return Hex.concat(
1241
1394
  serializedMultisigType,
1242
1395
  Rlp.fromHex([
1243
- multisig.account,
1244
- multisig.genesisConfigId,
1396
+ multisig.init
1397
+ ? MultisigConfig.toTuple(multisig.init)
1398
+ : multisig.account,
1245
1399
  multisig.signatures.map((signature) => serialize(signature)),
1246
- multisig.init ? MultisigConfig.toTuple(multisig.init) : '0x',
1247
1400
  ]),
1248
1401
  options.magic ? magicBytes : '0x',
1249
1402
  )
@@ -1260,6 +1413,16 @@ export declare namespace serialize {
1260
1413
  */
1261
1414
  magic?: boolean | undefined
1262
1415
  }
1416
+
1417
+ type ErrorType =
1418
+ | assert.ErrorType
1419
+ | CoercionError
1420
+ | Hex.concat.ErrorType
1421
+ | Hex.fromNumber.ErrorType
1422
+ | Hex.fromString.ErrorType
1423
+ | Rlp.fromHex.ErrorType
1424
+ | Signature.toHex.ErrorType
1425
+ | Errors.GlobalErrorType
1263
1426
  }
1264
1427
 
1265
1428
  /**
@@ -1271,10 +1434,10 @@ export declare namespace serialize {
1271
1434
  * ({@link ox#MultisigConfig.(getSignPayload:function)}), so the signer of
1272
1435
  * every approval is recovered against that digest and the list is sorted by the
1273
1436
  * recovered owner address. Works for any owner key type (secp256k1, p256,
1274
- * webAuthn, keychain).
1437
+ * webAuthn).
1275
1438
  *
1276
- * Config updates never change `account`/`genesisConfigId`, so the genesis
1277
- * config is the correct input even for post-update transactions.
1439
+ * Config updates never change `account`, so the genesis config is the correct
1440
+ * input even for post-update transactions.
1278
1441
  *
1279
1442
  * @example
1280
1443
  * ```ts twoslash
@@ -1315,12 +1478,7 @@ export function sortMultisigApprovals(
1315
1478
  const digest = MultisigConfig.getSignPayload(
1316
1479
  'genesisConfig' in value && value.genesisConfig
1317
1480
  ? { payload, genesisConfig: value.genesisConfig }
1318
- : {
1319
- payload,
1320
- account: (value as { account: Address.Address }).account,
1321
- genesisConfigId: (value as { genesisConfigId: Hex.Hex })
1322
- .genesisConfigId,
1323
- },
1481
+ : { payload, account: (value as { account: Address.Address }).account },
1324
1482
  )
1325
1483
  // Recover each signer once (decorate–sort–undecorate) rather than inside the
1326
1484
  // comparator.
@@ -1337,20 +1495,17 @@ export declare namespace sortMultisigApprovals {
1337
1495
  type Value = {
1338
1496
  /** The inner transaction sign payload (`tx.signature_hash()`). */
1339
1497
  payload: Hex.Hex | Bytes.Bytes
1340
- /** The primitive owner approvals to order. */
1498
+ /** The owner approvals to order. */
1341
1499
  signatures: readonly SignatureEnvelope[]
1342
1500
  } & OneOf<
1343
1501
  | {
1344
1502
  /** The native multisig account address. */
1345
1503
  account: Address.Address
1346
- /** The permanent config ID. */
1347
- genesisConfigId: Hex.Hex
1348
1504
  }
1349
1505
  | {
1350
1506
  /**
1351
1507
  * The initial multisig config (the bootstrap config that derived the
1352
- * permanent `account` and `genesisConfigId`). Used to derive both values
1353
- * automatically.
1508
+ * permanent `account`). Used to derive the account automatically.
1354
1509
  */
1355
1510
  genesisConfig: MultisigConfig.Config
1356
1511
  }
@@ -1378,7 +1533,9 @@ export declare namespace sortMultisigApprovals {
1378
1533
  * @param envelope - The signature envelope to convert.
1379
1534
  * @returns The RPC signature envelope with hex values.
1380
1535
  */
1381
- export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1536
+ export function toRpc<const envelope extends toRpc.Input>(
1537
+ envelope: envelope,
1538
+ ): toRpc.ReturnType<envelope> {
1382
1539
  const type = getType(envelope)
1383
1540
 
1384
1541
  if (type === 'secp256k1') {
@@ -1386,7 +1543,7 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1386
1543
  return {
1387
1544
  ...Signature.toRpc(secp256k1.signature),
1388
1545
  type: 'secp256k1',
1389
- }
1546
+ } as never
1390
1547
  }
1391
1548
 
1392
1549
  if (type === 'p256') {
@@ -1398,7 +1555,7 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1398
1555
  r: Hex.fromNumber(p256.signature.r, { size: 32 }),
1399
1556
  s: Hex.fromNumber(p256.signature.s, { size: 32 }),
1400
1557
  type: 'p256',
1401
- }
1558
+ } as never
1402
1559
  }
1403
1560
 
1404
1561
  if (type === 'webAuthn') {
@@ -1415,7 +1572,7 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1415
1572
  s: Hex.fromNumber(webauthn.signature.s, { size: 32 }),
1416
1573
  type: 'webAuthn',
1417
1574
  webauthnData,
1418
- }
1575
+ } as never
1419
1576
  }
1420
1577
 
1421
1578
  if (type === 'keychain') {
@@ -1426,28 +1583,57 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1426
1583
  signature: toRpc(keychain.inner),
1427
1584
  ...(keychain.keyId ? { keyId: keychain.keyId } : {}),
1428
1585
  ...(keychain.version ? { version: keychain.version } : {}),
1429
- }
1586
+ } as never
1430
1587
  }
1431
1588
 
1432
1589
  if (type === 'multisig') {
1433
1590
  const multisig = envelope as Multisig
1591
+ assert(multisig)
1592
+ const signatures = multisig.signatures.map((signature) => toRpc(signature))
1593
+ if (multisig.init) {
1594
+ const init = {
1595
+ ...multisig.init,
1596
+ salt: multisig.init.salt ?? MultisigConfig.zeroSalt,
1597
+ threshold: Number(multisig.init.threshold),
1598
+ owners: multisig.init.owners.map((owner) => ({
1599
+ ...owner,
1600
+ weight: Number(owner.weight),
1601
+ })),
1602
+ }
1603
+ return {
1604
+ init,
1605
+ signatures,
1606
+ } as never
1607
+ }
1434
1608
  return {
1435
- type: 'multisig',
1436
1609
  account: multisig.account,
1437
- // Map the typed envelope's `genesisConfigId` to the RPC wire field
1438
- // `configId` (TIP-1061 spec name).
1439
- configId: multisig.genesisConfigId,
1440
- // Owner approvals are raw serialized signatures (node `Vec<Bytes>`).
1441
- signatures: multisig.signatures.map((signature) => serialize(signature)),
1442
- ...(multisig.init ? { init: multisig.init } : {}),
1443
- }
1610
+ signatures,
1611
+ } as never
1444
1612
  }
1445
1613
 
1446
1614
  throw new CoercionError({ envelope })
1447
1615
  }
1448
1616
 
1449
1617
  export declare namespace toRpc {
1618
+ /** Input accepted by {@link ox#SignatureEnvelope.(toRpc:function)}. */
1619
+ type Input = SignatureEnvelope
1620
+
1621
+ /** RPC signature envelope inferred from the input type. */
1622
+ type ReturnType<envelope extends Input = Input> =
1623
+ GetType<envelope> extends 'secp256k1'
1624
+ ? Secp256k1Rpc
1625
+ : GetType<envelope> extends 'p256'
1626
+ ? P256Rpc
1627
+ : GetType<envelope> extends 'webAuthn'
1628
+ ? WebAuthnRpc
1629
+ : GetType<envelope> extends 'keychain'
1630
+ ? KeychainRpc
1631
+ : GetType<envelope> extends 'multisig'
1632
+ ? MultisigRpc
1633
+ : SignatureEnvelopeRpc
1634
+
1450
1635
  type ErrorType =
1636
+ | assert.ErrorType
1451
1637
  | CoercionError
1452
1638
  | Signature.toRpc.ErrorType
1453
1639
  | Errors.GlobalErrorType
@@ -1706,6 +1892,16 @@ export class InvalidSerializedError extends Errors.BaseError {
1706
1892
  }
1707
1893
  }
1708
1894
 
1895
+ /**
1896
+ * Error thrown when a native multisig owner approval is invalid.
1897
+ */
1898
+ export class InvalidMultisigApprovalError extends Errors.BaseError {
1899
+ override readonly name = 'SignatureEnvelope.InvalidMultisigApprovalError'
1900
+ constructor({ reason }: { reason: string }) {
1901
+ super(`Invalid native multisig owner approval: ${reason}.`)
1902
+ }
1903
+ }
1904
+
1709
1905
  /**
1710
1906
  * Error thrown when a signature envelope fails to verify.
1711
1907
  */