ox 0.14.25 → 0.14.26
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 +8 -0
- package/_cjs/tempo/KeyAuthorization.js +130 -57
- package/_cjs/tempo/KeyAuthorization.js.map +1 -1
- package/_cjs/version.js +1 -1
- package/_esm/tempo/KeyAuthorization.js +155 -62
- package/_esm/tempo/KeyAuthorization.js.map +1 -1
- package/_esm/version.js +1 -1
- package/_types/tempo/KeyAuthorization.d.ts +54 -2
- package/_types/tempo/KeyAuthorization.d.ts.map +1 -1
- package/_types/version.d.ts +1 -1
- package/package.json +2 -3
- package/tempo/KeyAuthorization.test.ts +333 -0
- package/tempo/KeyAuthorization.ts +240 -72
- package/tempo/e2e.test.ts +330 -0
- package/version.ts +1 -1
|
@@ -3,7 +3,7 @@ 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 } from '../core/internal/types.js'
|
|
6
|
+
import type { Compute, OneOf } from '../core/internal/types.js'
|
|
7
7
|
import * as Rlp from '../core/Rlp.js'
|
|
8
8
|
import * as SignatureEnvelope from './SignatureEnvelope.js'
|
|
9
9
|
import * as TempoAddress from './TempoAddress.js'
|
|
@@ -55,13 +55,38 @@ export type KeyAuthorization<
|
|
|
55
55
|
scopes?: readonly Scope<addressType>[] | undefined
|
|
56
56
|
/** Key type. (secp256k1, P256, WebAuthn). */
|
|
57
57
|
type: SignatureEnvelope.Type
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Optional 32-byte witness bound into the signing hash.
|
|
60
|
+
*
|
|
61
|
+
* Applications use this to bind a single signature to an arbitrary offchain
|
|
62
|
+
* context (e.g. a server-issued challenge), or as a revocation handle that
|
|
63
|
+
* can be burned onchain to invalidate the authorization before submission.
|
|
64
|
+
*
|
|
65
|
+
* [TIP-1053 Specification](https://tips.sh/1053)
|
|
66
|
+
*/
|
|
67
|
+
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)
|
|
75
|
+
| {
|
|
76
|
+
/** Account address this authorization is bound to. */
|
|
77
|
+
account: addressType
|
|
78
|
+
/** Whether this authorization provisions an admin access key. */
|
|
79
|
+
isAdmin: boolean
|
|
80
|
+
}
|
|
81
|
+
| {}
|
|
82
|
+
> &
|
|
83
|
+
(signed extends true
|
|
84
|
+
? { signature: SignatureEnvelope.SignatureEnvelope<bigintType, numberType> }
|
|
85
|
+
: {
|
|
86
|
+
signature?:
|
|
87
|
+
| SignatureEnvelope.SignatureEnvelope<bigintType, numberType>
|
|
88
|
+
| undefined
|
|
89
|
+
})
|
|
65
90
|
|
|
66
91
|
/** Input type for a Key Authorization. */
|
|
67
92
|
export type Input = KeyAuthorization<
|
|
@@ -73,12 +98,16 @@ export type Input = KeyAuthorization<
|
|
|
73
98
|
|
|
74
99
|
/** RPC representation matching the node's wire format. */
|
|
75
100
|
export type Rpc = {
|
|
101
|
+
/** Optional account address binding (TIP-1049). */
|
|
102
|
+
account?: Address.Address | null | undefined
|
|
76
103
|
/** Allowed call scopes (node field: `allowedCalls`). */
|
|
77
104
|
allowedCalls?: readonly RpcCallScope[] | undefined
|
|
78
105
|
/** Chain ID (hex quantity). */
|
|
79
106
|
chainId: Hex.Hex
|
|
80
107
|
/** Expiry timestamp (hex quantity or null). */
|
|
81
108
|
expiry: Hex.Hex | null | undefined
|
|
109
|
+
/** Whether this authorization provisions an admin access key (TIP-1049). */
|
|
110
|
+
isAdmin?: boolean | null | undefined
|
|
82
111
|
/** Key identifier. */
|
|
83
112
|
keyId: Address.Address
|
|
84
113
|
/** Key type. */
|
|
@@ -87,6 +116,8 @@ export type Rpc = {
|
|
|
87
116
|
limits?: readonly RpcTokenLimit[] | undefined
|
|
88
117
|
/** Signature envelope. */
|
|
89
118
|
signature: SignatureEnvelope.SignatureEnvelopeRpc
|
|
119
|
+
/** Optional 32-byte witness (hex). */
|
|
120
|
+
witness?: Hex.Hex | null | undefined
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
/** RPC representation of a token limit (matches node's `TokenLimit` serde). */
|
|
@@ -145,6 +176,30 @@ type AuthorizationTuple =
|
|
|
145
176
|
limits: readonly TokenLimitTuple[],
|
|
146
177
|
calls: readonly CallScopeTuple[],
|
|
147
178
|
]
|
|
179
|
+
| readonly [
|
|
180
|
+
...BaseTuple,
|
|
181
|
+
expiry: Hex.Hex,
|
|
182
|
+
limits: readonly TokenLimitTuple[],
|
|
183
|
+
calls: readonly CallScopeTuple[],
|
|
184
|
+
witness: Hex.Hex,
|
|
185
|
+
]
|
|
186
|
+
| readonly [
|
|
187
|
+
...BaseTuple,
|
|
188
|
+
expiry: Hex.Hex,
|
|
189
|
+
limits: readonly TokenLimitTuple[],
|
|
190
|
+
calls: readonly CallScopeTuple[],
|
|
191
|
+
witness: Hex.Hex,
|
|
192
|
+
isAdmin: Hex.Hex,
|
|
193
|
+
]
|
|
194
|
+
| readonly [
|
|
195
|
+
...BaseTuple,
|
|
196
|
+
expiry: Hex.Hex,
|
|
197
|
+
limits: readonly TokenLimitTuple[],
|
|
198
|
+
calls: readonly CallScopeTuple[],
|
|
199
|
+
witness: Hex.Hex,
|
|
200
|
+
isAdmin: Hex.Hex,
|
|
201
|
+
account: Address.Address,
|
|
202
|
+
]
|
|
148
203
|
|
|
149
204
|
/** Tuple representation of a Key Authorization. */
|
|
150
205
|
export type Tuple<signed extends boolean = boolean> = signed extends true
|
|
@@ -362,6 +417,7 @@ export function from<
|
|
|
362
417
|
recipients?: readonly TempoAddress.Address[]
|
|
363
418
|
}[]
|
|
364
419
|
}
|
|
420
|
+
if (auth.witness !== undefined) assertWitness(auth.witness)
|
|
365
421
|
const resolved = {
|
|
366
422
|
...auth,
|
|
367
423
|
address: TempoAddress.resolve(auth.address as TempoAddress.Address),
|
|
@@ -455,7 +511,11 @@ export declare namespace from {
|
|
|
455
511
|
export function fromRpc(authorization: Rpc): Signed {
|
|
456
512
|
const { allowedCalls, chainId, keyId, expiry, limits, keyType } =
|
|
457
513
|
authorization
|
|
514
|
+
const witness = authorization.witness ?? undefined
|
|
515
|
+
const isAdmin = authorization.isAdmin ?? undefined
|
|
516
|
+
const account = authorization.account ?? undefined
|
|
458
517
|
const signature = SignatureEnvelope.fromRpc(authorization.signature)
|
|
518
|
+
if (witness !== undefined) assertWitness(witness)
|
|
459
519
|
|
|
460
520
|
// Unflatten nested allowedCalls into flat scopes
|
|
461
521
|
const scopes = allowedCalls
|
|
@@ -488,6 +548,9 @@ export function fromRpc(authorization: Rpc): Signed {
|
|
|
488
548
|
...(scopes ? { scopes } : {}),
|
|
489
549
|
signature,
|
|
490
550
|
type: keyType,
|
|
551
|
+
...(witness !== undefined ? { witness } : {}),
|
|
552
|
+
...(isAdmin ? { isAdmin: true } : {}),
|
|
553
|
+
...(account !== undefined ? { account } : {}),
|
|
491
554
|
}
|
|
492
555
|
}
|
|
493
556
|
|
|
@@ -538,7 +601,13 @@ export function fromTuple<const tuple extends Tuple>(
|
|
|
538
601
|
tuple: tuple,
|
|
539
602
|
): fromTuple.ReturnType<tuple> {
|
|
540
603
|
const [authorization, signatureSerialized] = tuple
|
|
541
|
-
const [chainId, keyType_hex, keyId,
|
|
604
|
+
const [chainId, keyType_hex, keyId, ...trailing] =
|
|
605
|
+
authorization as unknown as [
|
|
606
|
+
Hex.Hex,
|
|
607
|
+
Hex.Hex,
|
|
608
|
+
Address.Address,
|
|
609
|
+
...unknown[],
|
|
610
|
+
]
|
|
542
611
|
const keyType = (() => {
|
|
543
612
|
switch (keyType_hex) {
|
|
544
613
|
case '0x':
|
|
@@ -552,54 +621,71 @@ export function fromTuple<const tuple extends Tuple>(
|
|
|
552
621
|
throw new Error(`Invalid key type: ${keyType_hex}`)
|
|
553
622
|
}
|
|
554
623
|
})()
|
|
624
|
+
// Trailing optional fields in wire order. Each entry pulls one slot off the
|
|
625
|
+
// trailing array and decodes it (treating absent or RLP-null placeholders as
|
|
626
|
+
// missing). To add a new optional trailing field, append a single entry.
|
|
627
|
+
const [rawExpiry, rawLimits, rawScopes, rawWitness, rawIsAdmin, rawAccount] =
|
|
628
|
+
trailing
|
|
629
|
+
const expiry = isAbsent(rawExpiry)
|
|
630
|
+
? undefined
|
|
631
|
+
: hexToNumber(rawExpiry as Hex.Hex) || undefined
|
|
632
|
+
const limits =
|
|
633
|
+
Array.isArray(rawLimits) && rawLimits.length > 0
|
|
634
|
+
? rawLimits.map((limitTuple: any) => {
|
|
635
|
+
const [token, limit, period] = limitTuple
|
|
636
|
+
return {
|
|
637
|
+
token,
|
|
638
|
+
limit: hexToBigint(limit),
|
|
639
|
+
...(period !== undefined ? { period: hexToNumber(period) } : {}),
|
|
640
|
+
}
|
|
641
|
+
})
|
|
642
|
+
: undefined
|
|
643
|
+
const scopes = Array.isArray(rawScopes)
|
|
644
|
+
? rawScopes.flatMap((scopeTuple: any) => {
|
|
645
|
+
const [address, selectorRules] = scopeTuple
|
|
646
|
+
// If no selector rules, this is an address-only scope.
|
|
647
|
+
if (!Array.isArray(selectorRules) || selectorRules.length === 0)
|
|
648
|
+
return [{ address }]
|
|
649
|
+
// Flatten each selector rule into a separate scope entry.
|
|
650
|
+
return selectorRules.map((ruleTuple: any) => {
|
|
651
|
+
const [selector, recipients] = ruleTuple
|
|
652
|
+
return {
|
|
653
|
+
address,
|
|
654
|
+
selector,
|
|
655
|
+
...(Array.isArray(recipients) && recipients.length > 0
|
|
656
|
+
? { recipients }
|
|
657
|
+
: {}),
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
})
|
|
661
|
+
: undefined
|
|
662
|
+
const witness = isAbsent(rawWitness) ? undefined : (rawWitness as Hex.Hex)
|
|
663
|
+
if (witness !== undefined) assertWitness(witness)
|
|
664
|
+
const isAdmin = (() => {
|
|
665
|
+
if (isAbsent(rawIsAdmin)) return undefined
|
|
666
|
+
// TIP-1049: the admin marker is strictly `0x01`. Any other value is a
|
|
667
|
+
// protocol-level decode error on the node, so reject it here too.
|
|
668
|
+
if (rawIsAdmin !== '0x01')
|
|
669
|
+
throw new InvalidAdminMarkerError(rawIsAdmin as Hex.Hex)
|
|
670
|
+
return true
|
|
671
|
+
})()
|
|
672
|
+
const account = isAbsent(rawAccount)
|
|
673
|
+
? undefined
|
|
674
|
+
: (rawAccount as Address.Address)
|
|
675
|
+
// TIP-1049 admin fields are paired: only emit both when both are present on
|
|
676
|
+
// the wire. Wire shapes carrying only one are tolerated for forward-compat
|
|
677
|
+
// but the orphan field is dropped (since the public API requires both).
|
|
678
|
+
const adminPair =
|
|
679
|
+
account !== undefined && isAdmin ? { account, isAdmin: true as const } : {}
|
|
555
680
|
const args: KeyAuthorization = {
|
|
556
681
|
address: keyId,
|
|
557
|
-
expiry:
|
|
558
|
-
typeof expiry !== 'undefined'
|
|
559
|
-
? hexToNumber(expiry) || undefined
|
|
560
|
-
: undefined,
|
|
561
|
-
type: keyType,
|
|
562
682
|
chainId: chainId === '0x' ? 0n : Hex.toBigInt(chainId),
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
...(
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
? {
|
|
570
|
-
limits: limits.map((limitTuple: any) => {
|
|
571
|
-
const [token, limit, period] = limitTuple
|
|
572
|
-
return {
|
|
573
|
-
token,
|
|
574
|
-
limit: hexToBigint(limit),
|
|
575
|
-
...(typeof period !== 'undefined'
|
|
576
|
-
? { period: hexToNumber(period) }
|
|
577
|
-
: {}),
|
|
578
|
-
}
|
|
579
|
-
}),
|
|
580
|
-
}
|
|
581
|
-
: {}),
|
|
582
|
-
...(typeof scopes !== 'undefined' && Array.isArray(scopes)
|
|
583
|
-
? {
|
|
584
|
-
scopes: scopes.flatMap((scopeTuple: any) => {
|
|
585
|
-
const [address, selectorRules] = scopeTuple
|
|
586
|
-
// If no selector rules, this is an address-only scope
|
|
587
|
-
if (!Array.isArray(selectorRules) || selectorRules.length === 0)
|
|
588
|
-
return [{ address }]
|
|
589
|
-
// Flatten each selector rule into a separate scope entry
|
|
590
|
-
return selectorRules.map((ruleTuple: any) => {
|
|
591
|
-
const [selector, recipients] = ruleTuple
|
|
592
|
-
return {
|
|
593
|
-
address,
|
|
594
|
-
selector,
|
|
595
|
-
...(Array.isArray(recipients) && recipients.length > 0
|
|
596
|
-
? { recipients }
|
|
597
|
-
: {}),
|
|
598
|
-
}
|
|
599
|
-
})
|
|
600
|
-
}),
|
|
601
|
-
}
|
|
602
|
-
: {}),
|
|
683
|
+
type: keyType,
|
|
684
|
+
...(expiry !== undefined ? { expiry } : {}),
|
|
685
|
+
...(limits !== undefined ? { limits } : {}),
|
|
686
|
+
...(scopes !== undefined ? { scopes } : {}),
|
|
687
|
+
...(witness !== undefined ? { witness } : {}),
|
|
688
|
+
...adminPair,
|
|
603
689
|
}
|
|
604
690
|
if (signatureSerialized)
|
|
605
691
|
args.signature = SignatureEnvelope.deserialize(signatureSerialized)
|
|
@@ -803,8 +889,19 @@ export declare namespace serialize {
|
|
|
803
889
|
* @returns An RPC-formatted Key Authorization.
|
|
804
890
|
*/
|
|
805
891
|
export function toRpc(authorization: Signed): Rpc {
|
|
806
|
-
const {
|
|
807
|
-
|
|
892
|
+
const {
|
|
893
|
+
address,
|
|
894
|
+
scopes,
|
|
895
|
+
chainId,
|
|
896
|
+
expiry,
|
|
897
|
+
limits,
|
|
898
|
+
type,
|
|
899
|
+
signature,
|
|
900
|
+
witness,
|
|
901
|
+
isAdmin,
|
|
902
|
+
account,
|
|
903
|
+
} = authorization
|
|
904
|
+
if (witness !== undefined) assertWitness(witness)
|
|
808
905
|
|
|
809
906
|
// Group flat scopes by address into nested allowedCalls wire format
|
|
810
907
|
const allowedCalls = (() => {
|
|
@@ -842,6 +939,9 @@ export function toRpc(authorization: Signed): Rpc {
|
|
|
842
939
|
})),
|
|
843
940
|
signature: SignatureEnvelope.toRpc(signature),
|
|
844
941
|
...(allowedCalls ? { allowedCalls } : {}),
|
|
942
|
+
...(witness !== undefined ? { witness } : {}),
|
|
943
|
+
...(isAdmin ? { isAdmin: true } : {}),
|
|
944
|
+
...(account !== undefined ? { account } : {}),
|
|
845
945
|
}
|
|
846
946
|
}
|
|
847
947
|
|
|
@@ -883,7 +983,17 @@ export declare namespace toRpc {
|
|
|
883
983
|
export function toTuple<const authorization extends KeyAuthorization>(
|
|
884
984
|
authorization: authorization,
|
|
885
985
|
): toTuple.ReturnType<authorization> {
|
|
886
|
-
const {
|
|
986
|
+
const {
|
|
987
|
+
address,
|
|
988
|
+
chainId,
|
|
989
|
+
scopes,
|
|
990
|
+
expiry,
|
|
991
|
+
limits,
|
|
992
|
+
witness,
|
|
993
|
+
isAdmin,
|
|
994
|
+
account,
|
|
995
|
+
} = authorization
|
|
996
|
+
if (witness !== undefined) assertWitness(witness)
|
|
887
997
|
const signature = authorization.signature
|
|
888
998
|
? SignatureEnvelope.serialize(authorization.signature)
|
|
889
999
|
: undefined
|
|
@@ -930,21 +1040,51 @@ export function toTuple<const authorization extends KeyAuthorization>(
|
|
|
930
1040
|
selectorRules.map(([selector, recipients]) => [selector, recipients]),
|
|
931
1041
|
])
|
|
932
1042
|
})()
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
1043
|
+
// Optional trailing fields in wire order. Each entry's `placeholder` is
|
|
1044
|
+
// emitted when this field is skipped but a later field is present.
|
|
1045
|
+
//
|
|
1046
|
+
// Placeholder convention:
|
|
1047
|
+
// - `'0x'` (RLP null) is the canonical placeholder for fields added at or
|
|
1048
|
+
// after TIP-1053. The node decodes it as `None` (unrestricted).
|
|
1049
|
+
// - `limits` keeps `[]` as its skipped placeholder for pre-TIP-1053 wire
|
|
1050
|
+
// shapes — preserving byte-for-byte equivalence with signed payloads
|
|
1051
|
+
// produced before TIP-1053 was added. When any TIP-1053+ field is
|
|
1052
|
+
// present, the canonical `'0x'` placeholder is used instead.
|
|
1053
|
+
//
|
|
1054
|
+
// To add a new optional trailing field (e.g. from a future TIP): append a
|
|
1055
|
+
// single entry to this list with `placeholder: '0x'`.
|
|
1056
|
+
const hasTip1053Plus =
|
|
1057
|
+
witness !== undefined || isAdmin || account !== undefined
|
|
1058
|
+
const optionals: readonly { value: unknown; placeholder: unknown }[] = [
|
|
1059
|
+
{
|
|
1060
|
+
value:
|
|
1061
|
+
expiry !== null && expiry !== undefined && expiry !== 0
|
|
1062
|
+
? numberToHex(expiry)
|
|
1063
|
+
: undefined,
|
|
1064
|
+
placeholder: '0x',
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
value: limitsValue,
|
|
1068
|
+
placeholder: hasTip1053Plus ? '0x' : [],
|
|
1069
|
+
},
|
|
1070
|
+
{ value: callsValue, placeholder: '0x' },
|
|
1071
|
+
{ value: witness, placeholder: '0x' },
|
|
1072
|
+
// TIP-1049: admin marker. Present = `0x01` (RLP integer 1); absent
|
|
1073
|
+
// skipped or omitted. Any other value is a hard decode error on the node.
|
|
1074
|
+
{ value: isAdmin ? '0x01' : undefined, placeholder: '0x' },
|
|
1075
|
+
// TIP-1049: optional account binding. Last field — never a placeholder.
|
|
1076
|
+
{ value: account, placeholder: '0x' },
|
|
1077
|
+
]
|
|
1078
|
+
let lastPresent = -1
|
|
1079
|
+
for (let i = optionals.length - 1; i >= 0; i--)
|
|
1080
|
+
if (optionals[i]!.value !== undefined) {
|
|
1081
|
+
lastPresent = i
|
|
1082
|
+
break
|
|
1083
|
+
}
|
|
1084
|
+
const trailing = optionals
|
|
1085
|
+
.slice(0, lastPresent + 1)
|
|
1086
|
+
.map(({ value, placeholder }) => value ?? placeholder)
|
|
1087
|
+
const authorizationTuple = [bigintToHex(chainId), type, address, ...trailing]
|
|
948
1088
|
return [authorizationTuple, ...(signature ? [signature] : [])] as never
|
|
949
1089
|
}
|
|
950
1090
|
|
|
@@ -985,3 +1125,31 @@ function resolveSelector(
|
|
|
985
1125
|
if (selector.startsWith('0x')) return selector as Hex.Hex
|
|
986
1126
|
return AbiItem.getSelector(selector)
|
|
987
1127
|
}
|
|
1128
|
+
|
|
1129
|
+
function assertWitness(witness: Hex.Hex): void {
|
|
1130
|
+
if (Hex.size(witness) !== 32) throw new InvalidWitnessSizeError(witness)
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function isAbsent(value: unknown): boolean {
|
|
1134
|
+
return value === undefined || value === '0x'
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/** Thrown when a `witness` field is not exactly 32 bytes. */
|
|
1138
|
+
export class InvalidWitnessSizeError extends Error {
|
|
1139
|
+
override readonly name = 'KeyAuthorization.InvalidWitnessSizeError'
|
|
1140
|
+
constructor(witness: Hex.Hex) {
|
|
1141
|
+
super(
|
|
1142
|
+
`Witness \`${witness}\` must be exactly 32 bytes (got ${Hex.size(witness)} bytes).`,
|
|
1143
|
+
)
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** Thrown when a TIP-1049 admin marker has any value other than `0x01`. */
|
|
1148
|
+
export class InvalidAdminMarkerError extends Error {
|
|
1149
|
+
override readonly name = 'KeyAuthorization.InvalidAdminMarkerError'
|
|
1150
|
+
constructor(marker: Hex.Hex) {
|
|
1151
|
+
super(
|
|
1152
|
+
`Admin marker \`${marker}\` is invalid; expected \`0x01\` (TIP-1049).`,
|
|
1153
|
+
)
|
|
1154
|
+
}
|
|
1155
|
+
}
|