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.
- package/CHANGELOG.md +12 -0
- package/_cjs/tempo/MultisigConfig.js +135 -0
- package/_cjs/tempo/MultisigConfig.js.map +1 -0
- package/_cjs/tempo/SignatureEnvelope.js +128 -6
- package/_cjs/tempo/SignatureEnvelope.js.map +1 -1
- package/_cjs/tempo/index.js +2 -1
- package/_cjs/tempo/index.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/MultisigConfig.js +363 -0
- package/_esm/tempo/MultisigConfig.js.map +1 -0
- package/_esm/tempo/SignatureEnvelope.js +237 -6
- package/_esm/tempo/SignatureEnvelope.js.map +1 -1
- package/_esm/tempo/index.js +26 -0
- package/_esm/tempo/index.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/tempo/MultisigConfig.d.ts +326 -0
- package/_types/tempo/MultisigConfig.d.ts.map +1 -0
- package/_types/tempo/SignatureEnvelope.d.ts +170 -8
- package/_types/tempo/SignatureEnvelope.d.ts.map +1 -1
- package/_types/tempo/index.d.ts +26 -0
- package/_types/tempo/index.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/package.json +6 -1
- package/tempo/MultisigConfig/package.json +6 -0
- package/tempo/MultisigConfig.test.ts +236 -0
- package/tempo/MultisigConfig.ts +501 -0
- package/tempo/SignatureEnvelope.test.ts +311 -2
- package/tempo/SignatureEnvelope.ts +368 -20
- package/tempo/e2e.test.ts +201 -0
- package/tempo/index.ts +26 -0
- package/version.ts +1 -1
|
@@ -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
|
-
:
|
|
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),
|
|
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
|
-
:
|
|
737
|
-
?
|
|
738
|
-
:
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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:
|
|
844
|
-
inner: fromRpc(
|
|
845
|
-
...(
|
|
846
|
-
...(
|
|
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)}`,
|