ox 0.14.25 → 0.14.27

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.
@@ -0,0 +1,263 @@
1
+ import * as AbiEvent from '../core/AbiEvent.js'
2
+ import * as AbiParameters from '../core/AbiParameters.js'
3
+ import type * as Address from '../core/Address.js'
4
+ import type * as Errors from '../core/Errors.js'
5
+ import type * as Hex from '../core/Hex.js'
6
+ import type { Compute } from '../core/internal/types.js'
7
+
8
+ /**
9
+ * A TIP-1028 receive-policy claim receipt: the ABI-encoded `ClaimReceiptV1`
10
+ * witness emitted when an inbound transfer or mint violates the recipient's
11
+ * receive policy.
12
+ *
13
+ * This is the canonical, on-chain representation – the value passed to the
14
+ * `ReceivePolicyGuard`'s `claim` and `burn` functions. Use `decode` to read its
15
+ * fields.
16
+ */
17
+ export type ReceivePolicyReceipt = Hex.Hex
18
+
19
+ /** Reason an inbound transfer or mint was blocked by a receive policy. */
20
+ export type BlockedReason = 'none' | 'tokenFilter' | 'receivePolicy'
21
+
22
+ /** Kind of inbound operation that was blocked. */
23
+ export type Kind = 'transfer' | 'mint'
24
+
25
+ /** A decoded {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt}. */
26
+ export type Decoded = Compute<{
27
+ /** Receipt layout version. */
28
+ version: number
29
+ /** TIP-20 token holding the blocked funds. */
30
+ token: Address.Address
31
+ /** Recovery authority captured when the operation was blocked. */
32
+ recoveryAuthority: Address.Address
33
+ /** Original sender (transfer) or issuer (mint). */
34
+ originator: Address.Address
35
+ /** Addressed recipient (may be a virtual address). */
36
+ recipient: Address.Address
37
+ /** Block timestamp when the operation was blocked. */
38
+ blockedAt: bigint
39
+ /** Guard nonce assigned when the operation was blocked. */
40
+ blockedNonce: bigint
41
+ /** Reason the operation was blocked. */
42
+ blockedReason: BlockedReason
43
+ /** Whether the blocked operation was a transfer or mint. */
44
+ kind: Kind
45
+ /** Application memo. */
46
+ memo: Hex.Hex
47
+ }>
48
+
49
+ /** @internal */
50
+ const blockedReasons = ['none', 'tokenFilter', 'receivePolicy'] as const
51
+
52
+ /** @internal */
53
+ const kinds = ['transfer', 'mint'] as const
54
+
55
+ /** @internal ABI parameters for the `ClaimReceiptV1` witness. */
56
+ const parameters = [
57
+ {
58
+ type: 'tuple',
59
+ components: [
60
+ { name: 'version', type: 'uint8' },
61
+ { name: 'token', type: 'address' },
62
+ { name: 'recoveryAuthority', type: 'address' },
63
+ { name: 'originator', type: 'address' },
64
+ { name: 'recipient', type: 'address' },
65
+ { name: 'blockedAt', type: 'uint64' },
66
+ { name: 'blockedNonce', type: 'uint64' },
67
+ { name: 'blockedReason', type: 'uint8' },
68
+ { name: 'kind', type: 'uint8' },
69
+ { name: 'memo', type: 'bytes32' },
70
+ ],
71
+ },
72
+ ] as const
73
+
74
+ /** @internal `TransferBlocked` event emitted by the `ReceivePolicyGuard`. */
75
+ const transferBlocked = AbiEvent.from(
76
+ 'event TransferBlocked(address indexed token, address indexed receiver, uint64 indexed blockedNonce, uint256 amount, uint8 receiptVersion, bytes receipt)',
77
+ )
78
+
79
+ /**
80
+ * Decodes a {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} (ABI-encoded
81
+ * `ClaimReceiptV1` witness) into its fields.
82
+ *
83
+ * [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
84
+ *
85
+ * @example
86
+ * ```ts twoslash
87
+ * import { ReceivePolicyReceipt } from 'ox/tempo'
88
+ *
89
+ * const decoded = ReceivePolicyReceipt.decode('0x...')
90
+ * ```
91
+ *
92
+ * @param receipt - The receive-policy receipt.
93
+ * @returns The decoded fields.
94
+ */
95
+ export function decode(receipt: ReceivePolicyReceipt): Decoded {
96
+ const [decoded] = AbiParameters.decode(parameters, receipt)
97
+ return {
98
+ version: decoded.version,
99
+ token: decoded.token,
100
+ recoveryAuthority: decoded.recoveryAuthority,
101
+ originator: decoded.originator,
102
+ recipient: decoded.recipient,
103
+ blockedAt: decoded.blockedAt,
104
+ blockedNonce: decoded.blockedNonce,
105
+ blockedReason: blockedReasons[decoded.blockedReason] ?? 'none',
106
+ kind: kinds[decoded.kind] ?? 'transfer',
107
+ memo: decoded.memo,
108
+ }
109
+ }
110
+
111
+ export declare namespace decode {
112
+ type ErrorType = AbiParameters.decode.ErrorType | Errors.GlobalErrorType
113
+ }
114
+
115
+ /**
116
+ * Encodes decoded fields into a
117
+ * {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt}. Inverse of `decode`.
118
+ *
119
+ * [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
120
+ *
121
+ * @example
122
+ * ```ts twoslash
123
+ * // @noErrors
124
+ * import { ReceivePolicyReceipt } from 'ox/tempo'
125
+ *
126
+ * const decoded = ReceivePolicyReceipt.decode('0x...')
127
+ * const receipt = ReceivePolicyReceipt.encode(decoded)
128
+ * ```
129
+ *
130
+ * @param decoded - The decoded fields.
131
+ * @returns The receive-policy receipt.
132
+ */
133
+ export function encode(decoded: Decoded): ReceivePolicyReceipt {
134
+ return AbiParameters.encode(parameters, [
135
+ {
136
+ version: decoded.version,
137
+ token: decoded.token,
138
+ recoveryAuthority: decoded.recoveryAuthority,
139
+ originator: decoded.originator,
140
+ recipient: decoded.recipient,
141
+ blockedAt: decoded.blockedAt,
142
+ blockedNonce: decoded.blockedNonce,
143
+ blockedReason: blockedReasons.indexOf(decoded.blockedReason),
144
+ kind: kinds.indexOf(decoded.kind),
145
+ memo: decoded.memo,
146
+ },
147
+ ])
148
+ }
149
+
150
+ export declare namespace encode {
151
+ type ErrorType = AbiParameters.encode.ErrorType | Errors.GlobalErrorType
152
+ }
153
+
154
+ /**
155
+ * Normalizes a {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} from either
156
+ * an encoded receipt (passthrough) or decoded fields.
157
+ *
158
+ * [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
159
+ *
160
+ * @example
161
+ * ```ts twoslash
162
+ * // @noErrors
163
+ * import { ReceivePolicyReceipt } from 'ox/tempo'
164
+ *
165
+ * // From an encoded receipt (passthrough).
166
+ * const a = ReceivePolicyReceipt.from('0x...')
167
+ *
168
+ * // From decoded fields.
169
+ * const b = ReceivePolicyReceipt.from(ReceivePolicyReceipt.decode('0x...'))
170
+ * ```
171
+ *
172
+ * @param value - An encoded receipt or decoded fields.
173
+ * @returns The receive-policy receipt.
174
+ */
175
+ export function from(
176
+ value: ReceivePolicyReceipt | Decoded,
177
+ ): ReceivePolicyReceipt {
178
+ if (typeof value === 'string') return value
179
+ return encode(value)
180
+ }
181
+
182
+ export declare namespace from {
183
+ type ErrorType = encode.ErrorType | Errors.GlobalErrorType
184
+ }
185
+
186
+ /**
187
+ * Extracts the {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} from a
188
+ * `ReceivePolicyGuard` `TransferBlocked` log.
189
+ *
190
+ * Throws if the log is not a `TransferBlocked` event. Use
191
+ * `fromTransactionReceipt` to extract every blocked transfer in a transaction
192
+ * (which skips unrelated logs).
193
+ *
194
+ * [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
195
+ *
196
+ * @example
197
+ * ```ts twoslash
198
+ * // @noErrors
199
+ * import { ReceivePolicyReceipt } from 'ox/tempo'
200
+ *
201
+ * const receipt = ReceivePolicyReceipt.fromLog(log)
202
+ * ```
203
+ *
204
+ * @param log - A `TransferBlocked` log (`data` & `topics`).
205
+ * @returns The receive-policy receipt.
206
+ */
207
+ export function fromLog(log: fromLog.Log): ReceivePolicyReceipt {
208
+ const { receipt } = AbiEvent.decode(transferBlocked, log)
209
+ return receipt
210
+ }
211
+
212
+ export declare namespace fromLog {
213
+ type Log = AbiEvent.decode.Log
214
+
215
+ type ErrorType = AbiEvent.decode.ErrorType | Errors.GlobalErrorType
216
+ }
217
+
218
+ /**
219
+ * Extracts every {@link ox#ReceivePolicyReceipt.ReceivePolicyReceipt} from a
220
+ * transaction receipt's logs.
221
+ *
222
+ * A single transaction may block multiple inbound transfers (e.g. a batched
223
+ * transfer to several recipients), so this returns an array – one entry per
224
+ * `TransferBlocked` log, in log order. Returns an empty array when no transfers
225
+ * were blocked.
226
+ *
227
+ * [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
228
+ *
229
+ * @example
230
+ * ```ts twoslash
231
+ * // @noErrors
232
+ * import { ReceivePolicyReceipt } from 'ox/tempo'
233
+ *
234
+ * const receipts = ReceivePolicyReceipt.fromTransactionReceipt(receipt)
235
+ * // @log: ['0x...'] (pass each to `claim` / `burn`)
236
+ * ```
237
+ *
238
+ * @param receipt - The transaction receipt (or any object with `logs`).
239
+ * @returns The receive-policy receipts, one per blocked transfer.
240
+ */
241
+ export function fromTransactionReceipt(
242
+ receipt: fromTransactionReceipt.Receipt,
243
+ ): readonly ReceivePolicyReceipt[] {
244
+ const selector = AbiEvent.getSelector(transferBlocked)
245
+ const receipts: ReceivePolicyReceipt[] = []
246
+ for (const log of receipt.logs ?? []) {
247
+ if (log.topics[0] !== selector) continue
248
+ receipts.push(fromLog(log))
249
+ }
250
+ return receipts
251
+ }
252
+
253
+ export declare namespace fromTransactionReceipt {
254
+ type Receipt = {
255
+ /** Logs emitted by the transaction. */
256
+ logs?: readonly AbiEvent.decode.Log[] | undefined
257
+ }
258
+
259
+ type ErrorType =
260
+ | AbiEvent.getSelector.ErrorType
261
+ | fromLog.ErrorType
262
+ | Errors.GlobalErrorType
263
+ }
package/tempo/e2e.test.ts CHANGED
@@ -2662,4 +2662,334 @@ describe('behavior: keyAuthorization', () => {
2662
2662
  ).rejects.toThrow()
2663
2663
  },
2664
2664
  )
2665
+
2666
+ // TODO: remove skipIf when devnet/testnet have T5 (TIP-1053).
2667
+ test.skipIf(nodeEnv !== 'localnet')(
2668
+ 'behavior: TIP-1053 witness round-trips through registration',
2669
+ async () => {
2670
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2671
+ const accessAddress = Address.fromPublicKey(
2672
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2673
+ )
2674
+
2675
+ // Application-defined challenge digest, bound to the authorization.
2676
+ const witness = Hex.random(32)
2677
+
2678
+ const keyAuth = KeyAuthorization.from({
2679
+ address: accessAddress,
2680
+ chainId: BigInt(chainId),
2681
+ type: 'secp256k1',
2682
+ witness,
2683
+ })
2684
+
2685
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2686
+ signature: SignatureEnvelope.from(
2687
+ Secp256k1.sign({
2688
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2689
+ privateKey: root.privateKey,
2690
+ }),
2691
+ ),
2692
+ })
2693
+
2694
+ const nonce = await getTransactionCount(client, {
2695
+ address: root.address,
2696
+ blockTag: 'pending',
2697
+ })
2698
+
2699
+ const transaction = TxEnvelopeTempo.from({
2700
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
2701
+ chainId,
2702
+ feeToken: '0x20c0000000000000000000000000000000000001',
2703
+ keyAuthorization: keyAuth_signed,
2704
+ nonce: BigInt(nonce),
2705
+ gas: 1_000_000n,
2706
+ maxFeePerGas: Value.fromGwei('20'),
2707
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2708
+ })
2709
+
2710
+ const signature = Secp256k1.sign({
2711
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2712
+ from: root.address,
2713
+ }),
2714
+ privateKey: accessPrivateKey,
2715
+ })
2716
+
2717
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2718
+ signature: SignatureEnvelope.from({
2719
+ userAddress: root.address,
2720
+ inner: SignatureEnvelope.from(signature),
2721
+ type: 'keychain',
2722
+ }),
2723
+ })
2724
+
2725
+ const receipt = (await client
2726
+ .request({
2727
+ method: 'eth_sendRawTransactionSync',
2728
+ params: [serialized_signed],
2729
+ })
2730
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
2731
+ expect(receipt.status).toBe('success')
2732
+
2733
+ const response = await client
2734
+ .request({
2735
+ method: 'eth_getTransactionByHash',
2736
+ params: [receipt.transactionHash],
2737
+ })
2738
+ .then((tx) => Transaction.fromRpc(tx as any))
2739
+ if (!response) throw new Error()
2740
+
2741
+ // The witness must survive the round trip through the node.
2742
+ expect(response.keyAuthorization?.witness).toBe(witness)
2743
+
2744
+ // The signing hash of the round-tripped authorization equals the witness-bearing hash.
2745
+ expect(KeyAuthorization.hash(response.keyAuthorization!)).toBe(
2746
+ KeyAuthorization.hash(keyAuth_signed),
2747
+ )
2748
+ },
2749
+ )
2750
+
2751
+ // TODO: remove skipIf when devnet/testnet have T5
2752
+ test.skipIf(nodeEnv !== 'localnet')(
2753
+ 'behavior: TIP-1053 witness-less authorization is byte-equivalent to pre-TIP-1053',
2754
+ async () => {
2755
+ // Two authorizations with identical fields but different access keys; the witness-less
2756
+ // shape must encode without a trailing witness slot.
2757
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2758
+ const accessAddress = Address.fromPublicKey(
2759
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2760
+ )
2761
+ const keyAuth = KeyAuthorization.from({
2762
+ address: accessAddress,
2763
+ chainId: BigInt(chainId),
2764
+ type: 'secp256k1',
2765
+ })
2766
+ const [authTuple] = KeyAuthorization.toTuple(keyAuth)
2767
+ expect((authTuple as unknown as unknown[]).length).toBe(3)
2768
+
2769
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2770
+ signature: SignatureEnvelope.from(
2771
+ Secp256k1.sign({
2772
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2773
+ privateKey: root.privateKey,
2774
+ }),
2775
+ ),
2776
+ })
2777
+
2778
+ const nonce = await getTransactionCount(client, {
2779
+ address: root.address,
2780
+ blockTag: 'pending',
2781
+ })
2782
+
2783
+ const transaction = TxEnvelopeTempo.from({
2784
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
2785
+ chainId,
2786
+ feeToken: '0x20c0000000000000000000000000000000000001',
2787
+ keyAuthorization: keyAuth_signed,
2788
+ nonce: BigInt(nonce),
2789
+ gas: 1_000_000n,
2790
+ maxFeePerGas: Value.fromGwei('20'),
2791
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2792
+ })
2793
+
2794
+ const signature = Secp256k1.sign({
2795
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2796
+ from: root.address,
2797
+ }),
2798
+ privateKey: accessPrivateKey,
2799
+ })
2800
+
2801
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2802
+ signature: SignatureEnvelope.from({
2803
+ userAddress: root.address,
2804
+ inner: SignatureEnvelope.from(signature),
2805
+ type: 'keychain',
2806
+ }),
2807
+ })
2808
+
2809
+ const receipt = (await client
2810
+ .request({
2811
+ method: 'eth_sendRawTransactionSync',
2812
+ params: [serialized_signed],
2813
+ })
2814
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
2815
+ expect(receipt.status).toBe('success')
2816
+
2817
+ const response = await client
2818
+ .request({
2819
+ method: 'eth_getTransactionByHash',
2820
+ params: [receipt.transactionHash],
2821
+ })
2822
+ .then((tx) => Transaction.fromRpc(tx as any))
2823
+ if (!response) throw new Error()
2824
+
2825
+ expect(response.keyAuthorization?.witness).toBeUndefined()
2826
+ },
2827
+ )
2828
+
2829
+ // TODO: remove skipIf when devnet/testnet have T6 (TIP-1049).
2830
+ test.skipIf(nodeEnv !== 'localnet')(
2831
+ 'behavior: TIP-1049 admin access key round-trips through registration',
2832
+ async () => {
2833
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2834
+ const accessAddress = Address.fromPublicKey(
2835
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2836
+ )
2837
+
2838
+ const keyAuth = KeyAuthorization.from({
2839
+ address: accessAddress,
2840
+ account: root.address,
2841
+ chainId: BigInt(chainId),
2842
+ isAdmin: true,
2843
+ type: 'secp256k1',
2844
+ })
2845
+
2846
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2847
+ signature: SignatureEnvelope.from(
2848
+ Secp256k1.sign({
2849
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2850
+ privateKey: root.privateKey,
2851
+ }),
2852
+ ),
2853
+ })
2854
+
2855
+ const nonce = await getTransactionCount(client, {
2856
+ address: root.address,
2857
+ blockTag: 'pending',
2858
+ })
2859
+
2860
+ const transaction = TxEnvelopeTempo.from({
2861
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
2862
+ chainId,
2863
+ feeToken: '0x20c0000000000000000000000000000000000001',
2864
+ keyAuthorization: keyAuth_signed,
2865
+ nonce: BigInt(nonce),
2866
+ gas: 1_000_000n,
2867
+ maxFeePerGas: Value.fromGwei('20'),
2868
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2869
+ })
2870
+
2871
+ // The admin access key signs and authorizes itself in the same tx
2872
+ // (the canonical "auth+use" registration pattern).
2873
+ const signature = Secp256k1.sign({
2874
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2875
+ from: root.address,
2876
+ }),
2877
+ privateKey: accessPrivateKey,
2878
+ })
2879
+
2880
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2881
+ signature: SignatureEnvelope.from({
2882
+ userAddress: root.address,
2883
+ inner: SignatureEnvelope.from(signature),
2884
+ type: 'keychain',
2885
+ }),
2886
+ })
2887
+
2888
+ const receipt = (await client
2889
+ .request({
2890
+ method: 'eth_sendRawTransactionSync',
2891
+ params: [serialized_signed],
2892
+ })
2893
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
2894
+ expect(receipt.status).toBe('success')
2895
+
2896
+ const response = await client
2897
+ .request({
2898
+ method: 'eth_getTransactionByHash',
2899
+ params: [receipt.transactionHash],
2900
+ })
2901
+ .then((tx) => Transaction.fromRpc(tx as any))
2902
+ if (!response) throw new Error()
2903
+
2904
+ // isAdmin + account must survive the round trip through the node.
2905
+ expect(response.keyAuthorization?.isAdmin).toBe(true)
2906
+ expect(response.keyAuthorization?.account).toBe(root.address)
2907
+
2908
+ // The signing hash of the round-tripped authorization equals the
2909
+ // admin-bearing hash.
2910
+ expect(KeyAuthorization.hash(response.keyAuthorization!)).toBe(
2911
+ KeyAuthorization.hash(keyAuth_signed),
2912
+ )
2913
+ },
2914
+ )
2915
+
2916
+ // TODO: remove skipIf when devnet/testnet have T6.
2917
+ test.skipIf(nodeEnv !== 'localnet')(
2918
+ 'behavior: TIP-1049 non-admin authorization is byte-equivalent to pre-TIP-1049',
2919
+ async () => {
2920
+ const accessPrivateKey = Secp256k1.randomPrivateKey()
2921
+ const accessAddress = Address.fromPublicKey(
2922
+ Secp256k1.getPublicKey({ privateKey: accessPrivateKey }),
2923
+ )
2924
+
2925
+ // Without isAdmin or account, the encoded tuple must not carry trailing
2926
+ // TIP-1049 slots.
2927
+ const keyAuth = KeyAuthorization.from({
2928
+ address: accessAddress,
2929
+ chainId: BigInt(chainId),
2930
+ type: 'secp256k1',
2931
+ })
2932
+ const [authTuple] = KeyAuthorization.toTuple(keyAuth)
2933
+ expect((authTuple as unknown as unknown[]).length).toBe(3)
2934
+
2935
+ const keyAuth_signed = KeyAuthorization.from(keyAuth, {
2936
+ signature: SignatureEnvelope.from(
2937
+ Secp256k1.sign({
2938
+ payload: KeyAuthorization.getSignPayload(keyAuth),
2939
+ privateKey: root.privateKey,
2940
+ }),
2941
+ ),
2942
+ })
2943
+
2944
+ const nonce = await getTransactionCount(client, {
2945
+ address: root.address,
2946
+ blockTag: 'pending',
2947
+ })
2948
+
2949
+ const transaction = TxEnvelopeTempo.from({
2950
+ calls: [{ to: '0x0000000000000000000000000000000000000000' }],
2951
+ chainId,
2952
+ feeToken: '0x20c0000000000000000000000000000000000001',
2953
+ keyAuthorization: keyAuth_signed,
2954
+ nonce: BigInt(nonce),
2955
+ gas: 1_000_000n,
2956
+ maxFeePerGas: Value.fromGwei('20'),
2957
+ maxPriorityFeePerGas: Value.fromGwei('10'),
2958
+ })
2959
+
2960
+ const signature = Secp256k1.sign({
2961
+ payload: TxEnvelopeTempo.getSignPayload(transaction, {
2962
+ from: root.address,
2963
+ }),
2964
+ privateKey: accessPrivateKey,
2965
+ })
2966
+
2967
+ const serialized_signed = TxEnvelopeTempo.serialize(transaction, {
2968
+ signature: SignatureEnvelope.from({
2969
+ userAddress: root.address,
2970
+ inner: SignatureEnvelope.from(signature),
2971
+ type: 'keychain',
2972
+ }),
2973
+ })
2974
+
2975
+ const receipt = (await client
2976
+ .request({
2977
+ method: 'eth_sendRawTransactionSync',
2978
+ params: [serialized_signed],
2979
+ })
2980
+ .then((tx) => TransactionReceipt.fromRpc(tx as any)))!
2981
+ expect(receipt.status).toBe('success')
2982
+
2983
+ const response = await client
2984
+ .request({
2985
+ method: 'eth_getTransactionByHash',
2986
+ params: [receipt.transactionHash],
2987
+ })
2988
+ .then((tx) => Transaction.fromRpc(tx as any))
2989
+ if (!response) throw new Error()
2990
+
2991
+ expect(response.keyAuthorization?.isAdmin).toBeUndefined()
2992
+ expect(response.keyAuthorization?.account).toBeUndefined()
2993
+ },
2994
+ )
2665
2995
  })
package/tempo/index.ts CHANGED
@@ -153,6 +153,28 @@ export * as Period from './Period.js'
153
153
  * @category Reference
154
154
  */
155
155
  export * as PoolId from './PoolId.js'
156
+ /**
157
+ * TIP-1028 receive-policy claim receipt utilities.
158
+ *
159
+ * When an inbound transfer or mint violates the recipient's receive policy, the
160
+ * funds are redirected to the `ReceivePolicyGuard` and a `ClaimReceiptV1`
161
+ * witness is emitted. This module decodes those witnesses (required to later
162
+ * `claim` or `burn` the blocked funds) from raw bytes or transaction receipts.
163
+ *
164
+ * [TIP-1028](https://docs.tempo.xyz/protocol/tips/tip-1028)
165
+ *
166
+ * @example
167
+ * ```ts twoslash
168
+ * // @noErrors
169
+ * import { ReceivePolicyReceipt } from 'ox/tempo'
170
+ *
171
+ * const receipts = ReceivePolicyReceipt.fromTransactionReceipt(receipt)
172
+ * const decoded = ReceivePolicyReceipt.decode('0x...')
173
+ * ```
174
+ *
175
+ * @category Reference
176
+ */
177
+ export * as ReceivePolicyReceipt from './ReceivePolicyReceipt.js'
156
178
  /**
157
179
  * Union of all JSON-RPC Methods for the `tempo_` namespace.
158
180
  *
package/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** @internal */
2
- export const version = '0.14.25'
2
+ export const version = '0.14.27'