ox 0.14.27 → 0.14.29

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.
@@ -13,16 +13,19 @@ import type {
13
13
  import * as Json from '../core/Json.js'
14
14
  import * as ox_P256 from '../core/P256.js'
15
15
  import type * as PublicKey from '../core/PublicKey.js'
16
+ import * as Rlp from '../core/Rlp.js'
16
17
  import * as ox_Secp256k1 from '../core/Secp256k1.js'
17
18
  import * as Signature from '../core/Signature.js'
18
19
  import type * as WebAuthnP256 from '../core/WebAuthnP256.js'
19
20
  import * as ox_WebAuthnP256 from '../core/WebAuthnP256.js'
21
+ import * as MultisigConfig from './MultisigConfig.js'
20
22
 
21
23
  /** Signature type identifiers for encoding/decoding */
22
24
  const serializedP256Type = '0x01'
23
25
  const serializedWebAuthnType = '0x02'
24
26
  const serializedKeychainType = '0x03'
25
27
  const serializedKeychainV2Type = '0x04'
28
+ const serializedMultisigType = '0x05'
26
29
 
27
30
  /** Serialized magic identifier for Tempo signature envelopes. */
28
31
  export const magicBytes =
@@ -69,7 +72,13 @@ export type GetType<
69
72
  userAddress: Address.Address
70
73
  }
71
74
  ? 'keychain'
72
- : never
75
+ : envelope extends {
76
+ account: Address.Address
77
+ genesisConfigId: `0x${string}`
78
+ signatures: any
79
+ }
80
+ ? 'multisig'
81
+ : never
73
82
 
74
83
  /**
75
84
  * Represents a signature envelope that can contain different signature types.
@@ -99,13 +108,14 @@ export type SignatureEnvelope<bigintType = bigint, numberType = number> = OneOf<
99
108
  | P256<bigintType, numberType>
100
109
  | WebAuthn<bigintType, numberType>
101
110
  | Keychain<bigintType, numberType>
111
+ | Multisig<bigintType, numberType>
102
112
  >
103
113
 
104
114
  /**
105
115
  * RPC-formatted signature envelope.
106
116
  */
107
117
  export type SignatureEnvelopeRpc = OneOf<
108
- Secp256k1Rpc | P256Rpc | WebAuthnRpc | KeychainRpc
118
+ Secp256k1Rpc | P256Rpc | WebAuthnRpc | KeychainRpc | MultisigRpc
109
119
  >
110
120
 
111
121
  /**
@@ -137,6 +147,48 @@ export type KeychainRpc = {
137
147
  version?: KeychainVersion | undefined
138
148
  }
139
149
 
150
+ /**
151
+ * Native multisig signature (type `0x05`).
152
+ *
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.
156
+ *
157
+ * [TIP-1061](https://tips.sh/1061)
158
+ */
159
+ export type Multisig<bigintType = bigint, numberType = number> = {
160
+ type: 'multisig'
161
+ /** Native multisig account address. */
162
+ 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. */
166
+ signatures: readonly SignatureEnvelope<bigintType, numberType>[]
167
+ /**
168
+ * Initial native multisig config for bootstrapping this account. Present only on
169
+ * the first (bootstrap) transaction from the derived account; absent on every
170
+ * subsequent transaction.
171
+ */
172
+ init?: MultisigConfig.Config<numberType> | undefined
173
+ }
174
+
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
+ }
191
+
140
192
  export type P256<bigintType = bigint, numberType = number> = {
141
193
  prehash: boolean
142
194
  publicKey: PublicKey.PublicKey
@@ -276,12 +328,26 @@ export function assert(envelope: PartialBy<SignatureEnvelope, 'type'>): void {
276
328
  assert(keychain.inner)
277
329
  return
278
330
  }
331
+
332
+ if (type === 'multisig') {
333
+ 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)
342
+ return
343
+ }
279
344
  }
280
345
 
281
346
  export declare namespace assert {
282
347
  type ErrorType =
283
348
  | CoercionError
284
349
  | MissingPropertiesError
350
+ | MultisigConfig.assert.ErrorType
285
351
  | Signature.assert.ErrorType
286
352
  | Errors.GlobalErrorType
287
353
  }
@@ -319,6 +385,9 @@ export function extractAddress(
319
385
  if (root) return signature.userAddress
320
386
  return extractAddress({ ...options, signature: signature.inner })
321
387
  }
388
+ // Native multisig signatures have no single signer; the recovered sender is the
389
+ // derived multisig account address.
390
+ if (signature.type === 'multisig') return signature.account
322
391
  return Address.fromPublicKey(extractPublicKey(options))
323
392
  }
324
393
 
@@ -381,6 +450,10 @@ export function extractPublicKey(
381
450
  return signature.publicKey
382
451
  case 'keychain':
383
452
  return extractPublicKey({ payload, signature: signature.inner })
453
+ case 'multisig':
454
+ // A multisig signature aggregates multiple owner approvals and has no
455
+ // single public key; recover the multisig account via `extractAddress`.
456
+ throw new CoercionError({ envelope: signature })
384
457
  }
385
458
  }
386
459
 
@@ -395,6 +468,7 @@ export declare namespace extractPublicKey {
395
468
  type ReturnType = PublicKey.PublicKey
396
469
 
397
470
  type ErrorType =
471
+ | CoercionError
398
472
  | ox_Secp256k1.recoverPublicKey.ErrorType
399
473
  | Errors.GlobalErrorType
400
474
  }
@@ -543,8 +617,31 @@ export function deserialize(value: Serialized): SignatureEnvelope {
543
617
  } satisfies Keychain
544
618
  }
545
619
 
620
+ 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 {
631
+ type: 'multisig',
632
+ 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
+ : {}),
640
+ } satisfies Multisig
641
+ }
642
+
546
643
  throw new InvalidSerializedError({
547
- reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256), ${serializedWebAuthnType} (WebAuthn), ${serializedKeychainType} (Keychain V1), or ${serializedKeychainV2Type} (Keychain V2)`,
644
+ reason: `Unknown signature type identifier: ${typeId}. Expected ${serializedP256Type} (P256), ${serializedWebAuthnType} (WebAuthn), ${serializedKeychainType} (Keychain V1), ${serializedKeychainV2Type} (Keychain V2), or ${serializedMultisigType} (Multisig)`,
548
645
  serialized,
549
646
  })
550
647
  }
@@ -661,6 +758,43 @@ export function deserialize(value: Serialized): SignatureEnvelope {
661
758
  * })
662
759
  * ```
663
760
  *
761
+ * @example
762
+ * ### Multisig (from genesis config)
763
+ *
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.
767
+ *
768
+ * ```ts twoslash
769
+ * import { Secp256k1 } from 'ox'
770
+ * import { MultisigConfig, SignatureEnvelope } from 'ox/tempo'
771
+ *
772
+ * const genesisConfig = MultisigConfig.from({
773
+ * threshold: 1,
774
+ * owners: [
775
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
776
+ * ],
777
+ * })
778
+ *
779
+ * const privateKey = Secp256k1.randomPrivateKey()
780
+ * const signature = SignatureEnvelope.from(
781
+ * Secp256k1.sign({ payload: '0xdeadbeef', privateKey }),
782
+ * )
783
+ *
784
+ * // Bootstrap transaction
785
+ * const bootstrap = SignatureEnvelope.from({
786
+ * genesisConfig,
787
+ * signatures: [signature],
788
+ * init: true,
789
+ * })
790
+ *
791
+ * // Subsequent (non-bootstrap) transactions
792
+ * const subsequent = SignatureEnvelope.from({
793
+ * genesisConfig,
794
+ * signatures: [signature],
795
+ * })
796
+ * ```
797
+ *
664
798
  * @param value - The value to coerce (either a hex string or signature envelope).
665
799
  * @returns The signature envelope.
666
800
  */
@@ -680,9 +814,44 @@ export function from<const value extends from.Value>(
680
814
  return { signature: value, type: 'secp256k1' } as never
681
815
 
682
816
  const type = getType(value)
817
+
818
+ if (type === 'multisig') {
819
+ const multisig = value as Multisig & {
820
+ genesisConfig?: MultisigConfig.Config | undefined
821
+ init?: MultisigConfig.Config | boolean | undefined
822
+ }
823
+ const { genesisConfig, init, ...rest } = multisig
824
+ // Derive `account`/`genesisConfigId` from `genesisConfig` when not provided
825
+ // explicitly.
826
+ const account = (() => {
827
+ if (rest.account) return rest.account
828
+ if (genesisConfig) return MultisigConfig.getAddress(genesisConfig)
829
+ return rest.account
830
+ })()
831
+ const genesisConfigId = (() => {
832
+ if (rest.genesisConfigId) return rest.genesisConfigId
833
+ if (genesisConfig) return MultisigConfig.toId(genesisConfig)
834
+ return rest.genesisConfigId
835
+ })()
836
+ // `init: true` opts into bootstrap using the supplied `genesisConfig`.
837
+ // Otherwise, `init` is treated as the explicit bootstrap config (or
838
+ // omitted).
839
+ const initSource = init === true ? genesisConfig : init || undefined
840
+ return {
841
+ ...rest,
842
+ account,
843
+ genesisConfigId,
844
+ signatures: rest.signatures.map((signature) => from(signature)),
845
+ // Normalize the bootstrap config (sorts owners, defaults the salt) so the
846
+ // in-memory envelope matches what `deserialize` reconstructs.
847
+ ...(initSource ? { init: MultisigConfig.from(initSource) } : {}),
848
+ type,
849
+ } as never
850
+ }
851
+
683
852
  return {
684
853
  ...value,
685
- ...(type === 'p256' ? { prehash: value.prehash } : {}),
854
+ ...(type === 'p256' ? { prehash: (value as P256).prehash } : {}),
686
855
  ...(type === 'keychain'
687
856
  ? {
688
857
  ...(!(
@@ -722,10 +891,24 @@ export declare namespace from {
722
891
  payload?: Hex.Hex | Bytes.Bytes | undefined
723
892
  }
724
893
 
894
+ /**
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
898
+ * (non-bootstrap) transactions.
899
+ */
900
+ type MultisigFromGenesisConfig = {
901
+ type?: 'multisig' | undefined
902
+ genesisConfig: MultisigConfig.Config
903
+ signatures: readonly SignatureEnvelope[]
904
+ init?: MultisigConfig.Config | boolean | undefined
905
+ }
906
+
725
907
  type Value =
726
908
  | UnionPartialBy<SignatureEnvelope, 'prehash' | 'type'>
727
909
  | Secp256k1Flat
728
910
  | Serialized
911
+ | MultisigFromGenesisConfig
729
912
 
730
913
  type ReturnValue<value extends Value> = Compute<
731
914
  OneOf<
@@ -733,16 +916,18 @@ export declare namespace from {
733
916
  ? SignatureEnvelope
734
917
  : value extends Secp256k1Flat
735
918
  ? Secp256k1
736
- : IsNarrowable<value, SignatureEnvelope> extends true
737
- ? SignatureEnvelope
738
- : Assign<
739
- value,
740
- {
741
- readonly type: GetType<value>
742
- } & (GetType<value> extends 'keychain'
743
- ? { keyId?: Address.Address | undefined }
744
- : {})
745
- >
919
+ : value extends MultisigFromGenesisConfig
920
+ ? Multisig
921
+ : IsNarrowable<value, SignatureEnvelope> extends true
922
+ ? SignatureEnvelope
923
+ : Assign<
924
+ value,
925
+ {
926
+ readonly type: GetType<value>
927
+ } & (GetType<value> extends 'keychain'
928
+ ? { keyId?: Address.Address | undefined }
929
+ : {})
930
+ >
746
931
  >
747
932
  >
748
933
  }
@@ -837,14 +1022,37 @@ export function fromRpc(envelope: SignatureEnvelopeRpc): SignatureEnvelope {
837
1022
  if (
838
1023
  envelope.type === 'keychain' ||
839
1024
  ('userAddress' in envelope && 'signature' in envelope)
840
- )
1025
+ ) {
1026
+ const keychain = envelope as KeychainRpc
841
1027
  return {
842
1028
  type: 'keychain',
843
- userAddress: envelope.userAddress,
844
- inner: fromRpc(envelope.signature),
845
- ...(envelope.keyId ? { keyId: envelope.keyId } : {}),
846
- ...(envelope.version ? { version: envelope.version } : {}),
1029
+ userAddress: keychain.userAddress,
1030
+ inner: fromRpc(keychain.signature),
1031
+ ...(keychain.keyId ? { keyId: keychain.keyId } : {}),
1032
+ ...(keychain.version ? { version: keychain.version } : {}),
847
1033
  }
1034
+ }
1035
+
1036
+ if (
1037
+ envelope.type === 'multisig' ||
1038
+ ('account' in envelope &&
1039
+ 'configId' in envelope &&
1040
+ 'signatures' in envelope)
1041
+ ) {
1042
+ const multisig = envelope as MultisigRpc
1043
+ return {
1044
+ 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
+ }
1055
+ }
848
1056
 
849
1057
  throw new CoercionError({ envelope })
850
1058
  }
@@ -922,6 +1130,14 @@ export function getType<
922
1130
  if ('userAddress' in envelope && 'inner' in envelope)
923
1131
  return 'keychain' as never
924
1132
 
1133
+ // Detect Multisig signature
1134
+ if (
1135
+ (('account' in envelope && 'genesisConfigId' in envelope) ||
1136
+ 'genesisConfig' in envelope) &&
1137
+ 'signatures' in envelope
1138
+ )
1139
+ return 'multisig' as never
1140
+
925
1141
  throw new CoercionError({
926
1142
  envelope,
927
1143
  })
@@ -1015,6 +1231,24 @@ export function serialize(
1015
1231
  )
1016
1232
  }
1017
1233
 
1234
+ if (type === 'multisig') {
1235
+ 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`).
1240
+ return Hex.concat(
1241
+ serializedMultisigType,
1242
+ Rlp.fromHex([
1243
+ multisig.account,
1244
+ multisig.genesisConfigId,
1245
+ multisig.signatures.map((signature) => serialize(signature)),
1246
+ multisig.init ? MultisigConfig.toTuple(multisig.init) : '0x',
1247
+ ]),
1248
+ options.magic ? magicBytes : '0x',
1249
+ )
1250
+ }
1251
+
1018
1252
  throw new CoercionError({ envelope })
1019
1253
  }
1020
1254
 
@@ -1028,6 +1262,106 @@ export declare namespace serialize {
1028
1262
  }
1029
1263
  }
1030
1264
 
1265
+ /**
1266
+ * Orders native multisig owner approvals into the strictly-ascending
1267
+ * recovered-owner order the Tempo node requires for the multisig `signatures`
1268
+ * array (the node enforces "recovered owners must be strictly ascending").
1269
+ *
1270
+ * Each approval is signed over the multisig owner approval digest
1271
+ * ({@link ox#MultisigConfig.(getSignPayload:function)}), so the signer of
1272
+ * every approval is recovered against that digest and the list is sorted by the
1273
+ * recovered owner address. Works for any owner key type (secp256k1, p256,
1274
+ * webAuthn, keychain).
1275
+ *
1276
+ * Config updates never change `account`/`genesisConfigId`, so the genesis
1277
+ * config is the correct input even for post-update transactions.
1278
+ *
1279
+ * @example
1280
+ * ```ts twoslash
1281
+ * import { Secp256k1 } from 'ox'
1282
+ * import { MultisigConfig, SignatureEnvelope, TxEnvelopeTempo } from 'ox/tempo'
1283
+ *
1284
+ * const genesisConfig = MultisigConfig.from({
1285
+ * threshold: 2,
1286
+ * owners: [
1287
+ * { owner: '0x1111111111111111111111111111111111111111', weight: 1 },
1288
+ * { owner: '0x2222222222222222222222222222222222222222', weight: 1 },
1289
+ * ],
1290
+ * })
1291
+ *
1292
+ * const tx = TxEnvelopeTempo.from({ chainId: 1, calls: [] })
1293
+ * const payload = TxEnvelopeTempo.getSignPayload(tx)
1294
+ *
1295
+ * const privateKeys = [Secp256k1.randomPrivateKey(), Secp256k1.randomPrivateKey()]
1296
+ * const digest = MultisigConfig.getSignPayload({ payload, genesisConfig })
1297
+ * const signatures = privateKeys.map((privateKey) =>
1298
+ * SignatureEnvelope.from(Secp256k1.sign({ payload: digest, privateKey })),
1299
+ * )
1300
+ *
1301
+ * const ordered = SignatureEnvelope.sortMultisigApprovals({ // [!code focus]
1302
+ * genesisConfig, // [!code focus]
1303
+ * payload, // [!code focus]
1304
+ * signatures, // [!code focus]
1305
+ * }) // [!code focus]
1306
+ * ```
1307
+ *
1308
+ * @param value - The approval ordering parameters.
1309
+ * @returns The owner approvals ordered ascending by recovered owner address.
1310
+ */
1311
+ export function sortMultisigApprovals(
1312
+ value: sortMultisigApprovals.Value,
1313
+ ): readonly SignatureEnvelope[] {
1314
+ const { payload, signatures } = value
1315
+ const digest = MultisigConfig.getSignPayload(
1316
+ 'genesisConfig' in value && value.genesisConfig
1317
+ ? { 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
+ },
1324
+ )
1325
+ // Recover each signer once (decorate–sort–undecorate) rather than inside the
1326
+ // comparator.
1327
+ return signatures
1328
+ .map((signature) => ({
1329
+ key: Hex.toBigInt(extractAddress({ payload: digest, signature })),
1330
+ signature,
1331
+ }))
1332
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
1333
+ .map((entry) => entry.signature)
1334
+ }
1335
+
1336
+ export declare namespace sortMultisigApprovals {
1337
+ type Value = {
1338
+ /** The inner transaction sign payload (`tx.signature_hash()`). */
1339
+ payload: Hex.Hex | Bytes.Bytes
1340
+ /** The primitive owner approvals to order. */
1341
+ signatures: readonly SignatureEnvelope[]
1342
+ } & OneOf<
1343
+ | {
1344
+ /** The native multisig account address. */
1345
+ account: Address.Address
1346
+ /** The permanent config ID. */
1347
+ genesisConfigId: Hex.Hex
1348
+ }
1349
+ | {
1350
+ /**
1351
+ * The initial multisig config (the bootstrap config that derived the
1352
+ * permanent `account` and `genesisConfigId`). Used to derive both values
1353
+ * automatically.
1354
+ */
1355
+ genesisConfig: MultisigConfig.Config
1356
+ }
1357
+ >
1358
+
1359
+ type ErrorType =
1360
+ | MultisigConfig.getSignPayload.ErrorType
1361
+ | extractAddress.ErrorType
1362
+ | Errors.GlobalErrorType
1363
+ }
1364
+
1031
1365
  /**
1032
1366
  * Converts a signature envelope to RPC format.
1033
1367
  *
@@ -1095,6 +1429,20 @@ export function toRpc(envelope: SignatureEnvelope): SignatureEnvelopeRpc {
1095
1429
  }
1096
1430
  }
1097
1431
 
1432
+ if (type === 'multisig') {
1433
+ const multisig = envelope as Multisig
1434
+ return {
1435
+ type: 'multisig',
1436
+ 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
+ }
1444
+ }
1445
+
1098
1446
  throw new CoercionError({ envelope })
1099
1447
  }
1100
1448
 
@@ -1332,7 +1680,7 @@ export class MissingPropertiesError extends Errors.BaseError {
1332
1680
  }: {
1333
1681
  envelope: unknown
1334
1682
  missing: string[]
1335
- type: Type
1683
+ type: Type | 'keychain' | 'multisig'
1336
1684
  }) {
1337
1685
  super(
1338
1686
  `Signature envelope of type "${type}" is missing required properties: ${missing.map((m) => `\`${m}\``).join(', ')}.\n\nProvided: ${Json.stringify(envelope)}`,