mppx 0.4.10 → 0.4.12
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 +23 -1
- package/dist/internal/env.d.ts +1 -1
- package/dist/internal/env.d.ts.map +1 -1
- package/dist/internal/env.js +2 -6
- package/dist/internal/env.js.map +1 -1
- package/dist/internal/types.d.ts +23 -0
- package/dist/internal/types.d.ts.map +1 -1
- package/dist/server/Mppx.d.ts +1 -1
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +49 -2
- package/dist/server/Mppx.js.map +1 -1
- package/dist/stripe/internal/types.d.ts +3 -0
- package/dist/stripe/internal/types.d.ts.map +1 -1
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js +9 -2
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/tempo/Methods.d.ts +15 -0
- package/dist/tempo/Methods.d.ts.map +1 -1
- package/dist/tempo/Methods.js +27 -3
- package/dist/tempo/Methods.js.map +1 -1
- package/dist/tempo/client/Charge.d.ts +21 -0
- package/dist/tempo/client/Charge.d.ts.map +1 -1
- package/dist/tempo/client/Charge.js +33 -7
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/client/Methods.d.ts +15 -0
- package/dist/tempo/client/Methods.d.ts.map +1 -1
- package/dist/tempo/internal/account.d.ts +5 -11
- package/dist/tempo/internal/account.d.ts.map +1 -1
- package/dist/tempo/internal/charge.d.ts +20 -0
- package/dist/tempo/internal/charge.d.ts.map +1 -0
- package/dist/tempo/internal/charge.js +23 -0
- package/dist/tempo/internal/charge.js.map +1 -0
- package/dist/tempo/internal/fee-payer.d.ts.map +1 -1
- package/dist/tempo/internal/fee-payer.js +15 -3
- package/dist/tempo/internal/fee-payer.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +17 -2
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +148 -99
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/Methods.d.ts +17 -2
- package/dist/tempo/server/Methods.d.ts.map +1 -1
- package/dist/tempo/server/Methods.js +4 -1
- package/dist/tempo/server/Methods.js.map +1 -1
- package/dist/tempo/server/Session.d.ts +9 -4
- package/dist/tempo/server/Session.d.ts.map +1 -1
- package/dist/tempo/server/Session.js +25 -6
- package/dist/tempo/server/Session.js.map +1 -1
- package/dist/tempo/session/Chain.d.ts +18 -2
- package/dist/tempo/session/Chain.d.ts.map +1 -1
- package/dist/tempo/session/Chain.js +18 -14
- package/dist/tempo/session/Chain.js.map +1 -1
- package/package.json +1 -1
- package/src/internal/env.test.ts +12 -12
- package/src/internal/env.ts +2 -6
- package/src/internal/types.ts +25 -0
- package/src/server/Mppx.test.ts +287 -0
- package/src/server/Mppx.ts +59 -5
- package/src/stripe/internal/types.ts +5 -1
- package/src/stripe/server/Charge.test.ts +52 -1
- package/src/stripe/server/Charge.ts +12 -4
- package/src/tempo/Methods.test.ts +79 -0
- package/src/tempo/Methods.ts +53 -17
- package/src/tempo/client/Charge.ts +41 -8
- package/src/tempo/internal/account.ts +7 -14
- package/src/tempo/internal/charge.ts +43 -0
- package/src/tempo/internal/fee-payer.test.ts +33 -14
- package/src/tempo/internal/fee-payer.ts +21 -6
- package/src/tempo/server/Charge.test.ts +231 -0
- package/src/tempo/server/Charge.ts +193 -124
- package/src/tempo/server/Methods.ts +4 -1
- package/src/tempo/server/Session.test.ts +57 -0
- package/src/tempo/server/Session.ts +33 -20
- package/src/tempo/session/Chain.test.ts +25 -5
- package/src/tempo/session/Chain.ts +30 -14
package/src/tempo/Methods.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type { Account } from 'viem'
|
|
1
|
+
import type { Account, Address } from 'viem'
|
|
2
2
|
import { parseUnits } from 'viem'
|
|
3
3
|
|
|
4
4
|
import * as Method from '../Method.js'
|
|
5
5
|
import * as z from '../zod.js'
|
|
6
6
|
|
|
7
|
+
const split = z.object({
|
|
8
|
+
amount: z.amount(),
|
|
9
|
+
memo: z.optional(z.hash()),
|
|
10
|
+
recipient: z.pipe(
|
|
11
|
+
z.string(),
|
|
12
|
+
z.transform((v) => v as Address),
|
|
13
|
+
),
|
|
14
|
+
})
|
|
15
|
+
|
|
7
16
|
/**
|
|
8
17
|
* Tempo charge intent for one-time TIP-20 token transfers.
|
|
9
18
|
*
|
|
@@ -20,31 +29,58 @@ export const charge = Method.from({
|
|
|
20
29
|
]),
|
|
21
30
|
},
|
|
22
31
|
request: z.pipe(
|
|
23
|
-
z
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
z.
|
|
32
|
-
z.
|
|
33
|
-
|
|
32
|
+
z
|
|
33
|
+
.object({
|
|
34
|
+
amount: z.amount(),
|
|
35
|
+
chainId: z.optional(z.number()),
|
|
36
|
+
currency: z.string(),
|
|
37
|
+
decimals: z.number(),
|
|
38
|
+
description: z.optional(z.string()),
|
|
39
|
+
externalId: z.optional(z.string()),
|
|
40
|
+
feePayer: z.optional(
|
|
41
|
+
z.pipe(
|
|
42
|
+
z.union([z.boolean(), z.custom<Account>()]),
|
|
43
|
+
z.transform((v): boolean => (typeof v === 'object' ? true : v)),
|
|
44
|
+
),
|
|
34
45
|
),
|
|
46
|
+
memo: z.optional(z.hash()),
|
|
47
|
+
recipient: z.optional(z.string()),
|
|
48
|
+
splits: z.optional(z.array(split).check(z.minLength(1), z.maxLength(10))),
|
|
49
|
+
})
|
|
50
|
+
.check(
|
|
51
|
+
z.refine(({ amount, decimals, splits }) => {
|
|
52
|
+
if (!splits) return true
|
|
53
|
+
|
|
54
|
+
const totalAmount = parseUnits(amount, decimals)
|
|
55
|
+
const splitTotal = splits.reduce(
|
|
56
|
+
(sum, split) => sum + parseUnits(split.amount, decimals),
|
|
57
|
+
0n,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
splits.every((split) => parseUnits(split.amount, decimals) > 0n) &&
|
|
62
|
+
splitTotal < totalAmount
|
|
63
|
+
)
|
|
64
|
+
}, 'Invalid splits'),
|
|
35
65
|
),
|
|
36
|
-
|
|
37
|
-
recipient: z.optional(z.string()),
|
|
38
|
-
}),
|
|
39
|
-
z.transform(({ amount, chainId, decimals, feePayer, memo, ...rest }) => ({
|
|
66
|
+
z.transform(({ amount, chainId, decimals, feePayer, memo, splits, ...rest }) => ({
|
|
40
67
|
...rest,
|
|
41
68
|
amount: parseUnits(amount, decimals).toString(),
|
|
42
|
-
...(chainId !== undefined ||
|
|
69
|
+
...(chainId !== undefined ||
|
|
70
|
+
feePayer !== undefined ||
|
|
71
|
+
memo !== undefined ||
|
|
72
|
+
splits !== undefined
|
|
43
73
|
? {
|
|
44
74
|
methodDetails: {
|
|
45
75
|
...(chainId !== undefined && { chainId }),
|
|
46
76
|
...(feePayer !== undefined && { feePayer }),
|
|
47
77
|
...(memo !== undefined && { memo }),
|
|
78
|
+
...(splits !== undefined && {
|
|
79
|
+
splits: splits.map((split) => ({
|
|
80
|
+
...split,
|
|
81
|
+
amount: parseUnits(split.amount, decimals).toString(),
|
|
82
|
+
})),
|
|
83
|
+
}),
|
|
48
84
|
},
|
|
49
85
|
}
|
|
50
86
|
: {}),
|
|
@@ -11,6 +11,7 @@ import * as Client from '../../viem/Client.js'
|
|
|
11
11
|
import * as z from '../../zod.js'
|
|
12
12
|
import * as Attribution from '../Attribution.js'
|
|
13
13
|
import * as AutoSwap from '../internal/auto-swap.js'
|
|
14
|
+
import * as Charge_internal from '../internal/charge.js'
|
|
14
15
|
import * as defaults from '../internal/defaults.js'
|
|
15
16
|
import * as Methods from '../Methods.js'
|
|
16
17
|
|
|
@@ -54,18 +55,37 @@ export function charge(parameters: charge.Parameters = {}) {
|
|
|
54
55
|
const { request } = challenge
|
|
55
56
|
const { amount, methodDetails } = request
|
|
56
57
|
const currency = request.currency as Address
|
|
57
|
-
|
|
58
|
+
|
|
59
|
+
if (parameters.expectedRecipients) {
|
|
60
|
+
const allowed = new Set(parameters.expectedRecipients.map((a) => a.toLowerCase()))
|
|
61
|
+
const splits = methodDetails?.splits as readonly { recipient: string }[] | undefined
|
|
62
|
+
if (splits) {
|
|
63
|
+
for (const split of splits) {
|
|
64
|
+
if (!allowed.has(split.recipient.toLowerCase()))
|
|
65
|
+
throw new Error(`Unexpected split recipient: ${split.recipient}`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
58
69
|
|
|
59
70
|
const memo = methodDetails?.memo
|
|
60
71
|
? (methodDetails.memo as Hex.Hex)
|
|
61
72
|
: Attribution.encode({ serverId: challenge.realm, clientId })
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
const transfers = Charge_internal.getTransfers({
|
|
74
|
+
amount,
|
|
75
|
+
methodDetails: {
|
|
76
|
+
...methodDetails,
|
|
77
|
+
memo,
|
|
78
|
+
},
|
|
79
|
+
recipient: request.recipient as Address,
|
|
68
80
|
})
|
|
81
|
+
const transferCalls = transfers.map((transfer) =>
|
|
82
|
+
Actions.token.transfer.call({
|
|
83
|
+
amount: BigInt(transfer.amount),
|
|
84
|
+
...(transfer.memo && { memo: transfer.memo as Hex.Hex }),
|
|
85
|
+
to: transfer.recipient as Address,
|
|
86
|
+
token: currency,
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
69
89
|
|
|
70
90
|
const autoSwap = AutoSwap.resolve(
|
|
71
91
|
context?.autoSwap ?? parameters.autoSwap,
|
|
@@ -82,7 +102,14 @@ export function charge(parameters: charge.Parameters = {}) {
|
|
|
82
102
|
})
|
|
83
103
|
: undefined
|
|
84
104
|
|
|
85
|
-
const calls = [...(swapCalls ?? []),
|
|
105
|
+
const calls = [...(swapCalls ?? []), ...transferCalls]
|
|
106
|
+
|
|
107
|
+
const validBefore = (() => {
|
|
108
|
+
const defaultExpiry = Math.floor(Date.now() / 1000) + 25
|
|
109
|
+
if (!challenge.expires) return defaultExpiry
|
|
110
|
+
const challengeExpiry = Math.floor(new Date(challenge.expires).getTime() / 1000)
|
|
111
|
+
return Math.min(defaultExpiry, challengeExpiry)
|
|
112
|
+
})()
|
|
86
113
|
|
|
87
114
|
if (mode === 'push') {
|
|
88
115
|
const { receipts } = await sendCallsSync(client, {
|
|
@@ -104,6 +131,7 @@ export function charge(parameters: charge.Parameters = {}) {
|
|
|
104
131
|
calls,
|
|
105
132
|
...(methodDetails?.feePayer && { feePayer: true }),
|
|
106
133
|
nonceKey: 'expiring',
|
|
134
|
+
validBefore,
|
|
107
135
|
} as never)
|
|
108
136
|
// FIXME: figure out gas estimation issue for fee payer tx
|
|
109
137
|
prepared.gas = prepared.gas! + 5_000n
|
|
@@ -131,6 +159,11 @@ export declare namespace charge {
|
|
|
131
159
|
autoSwap?: AutoSwap | undefined
|
|
132
160
|
/** Client identifier used to derive the client fingerprint in attribution memos. */
|
|
133
161
|
clientId?: string | undefined
|
|
162
|
+
/**
|
|
163
|
+
* Allowlist of expected split recipient addresses. When set, the client
|
|
164
|
+
* rejects any challenge whose split recipients are not in this list.
|
|
165
|
+
*/
|
|
166
|
+
expectedRecipients?: readonly Address[] | undefined
|
|
134
167
|
/**
|
|
135
168
|
* Controls how the charge transaction is submitted.
|
|
136
169
|
*
|
|
@@ -33,18 +33,11 @@ export function resolve(parameters: resolve.Parameters) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export declare namespace resolve {
|
|
36
|
-
type Parameters = {
|
|
37
|
-
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| {
|
|
44
|
-
/** Address that receives payment. */
|
|
45
|
-
account?: Address | undefined
|
|
46
|
-
/** Optional fee payer account or fee payer URL for covering transaction fees. */
|
|
47
|
-
feePayer?: Account | string | undefined
|
|
48
|
-
}
|
|
49
|
-
)
|
|
36
|
+
type Parameters = {
|
|
37
|
+
recipient?: Address | undefined
|
|
38
|
+
/** Account or address that performs payment operations / receives payment. */
|
|
39
|
+
account?: Account | Address | undefined
|
|
40
|
+
/** When `true`, the account also sponsors fees. An `Account` object or URL string can also be provided as a dedicated fee payer. */
|
|
41
|
+
feePayer?: Account | string | true | undefined
|
|
42
|
+
}
|
|
50
43
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Address } from 'viem'
|
|
2
|
+
|
|
3
|
+
export type Split = {
|
|
4
|
+
amount: string
|
|
5
|
+
memo?: string | undefined
|
|
6
|
+
recipient: Address
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type Transfer = {
|
|
10
|
+
amount: string
|
|
11
|
+
memo?: string | undefined
|
|
12
|
+
recipient: Address
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getTransfers(request: {
|
|
16
|
+
amount: string
|
|
17
|
+
methodDetails?: { memo?: string | undefined; splits?: readonly Split[] | undefined }
|
|
18
|
+
recipient: Address
|
|
19
|
+
}): Transfer[] {
|
|
20
|
+
const totalAmount = BigInt(request.amount)
|
|
21
|
+
const splits = request.methodDetails?.splits ?? []
|
|
22
|
+
|
|
23
|
+
const splitTotal = splits.reduce((sum, split) => sum + BigInt(split.amount), 0n)
|
|
24
|
+
if (splitTotal >= totalAmount)
|
|
25
|
+
throw new Error('Invalid charge request: split total must be less than total amount.')
|
|
26
|
+
|
|
27
|
+
const primaryAmount = totalAmount - splitTotal
|
|
28
|
+
if (primaryAmount <= 0n)
|
|
29
|
+
throw new Error('Invalid charge request: primary transfer amount must be positive.')
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
amount: primaryAmount.toString(),
|
|
34
|
+
memo: request.methodDetails?.memo,
|
|
35
|
+
recipient: request.recipient,
|
|
36
|
+
},
|
|
37
|
+
...splits.map((split) => ({
|
|
38
|
+
amount: split.amount,
|
|
39
|
+
memo: split.memo,
|
|
40
|
+
recipient: split.recipient,
|
|
41
|
+
})),
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -70,17 +70,7 @@ describe('validateCalls', () => {
|
|
|
70
70
|
).not.toThrow()
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
-
test('
|
|
74
|
-
expect(() => validateCalls([], details)).toThrow(FeePayerValidationError)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
test('error: rejects unknown selector', () => {
|
|
78
|
-
expect(() => validateCalls([{ data: '0xdeadbeef' as `0x${string}` }], details)).toThrow(
|
|
79
|
-
'disallowed call pattern',
|
|
80
|
-
)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
test('error: rejects extra calls beyond allowed patterns', () => {
|
|
73
|
+
test('accepts multiple transfers after swap prefix', () => {
|
|
84
74
|
const swapSelector = Selectors.swapExactAmountOut
|
|
85
75
|
expect(() =>
|
|
86
76
|
validateCalls(
|
|
@@ -100,19 +90,48 @@ describe('validateCalls', () => {
|
|
|
100
90
|
data: encodeFunctionData({
|
|
101
91
|
abi: Abis.tip20,
|
|
102
92
|
functionName: 'transfer',
|
|
103
|
-
args: [bogus,
|
|
93
|
+
args: [bogus, 90n],
|
|
104
94
|
}),
|
|
105
95
|
},
|
|
106
96
|
{
|
|
107
97
|
data: encodeFunctionData({
|
|
108
98
|
abi: Abis.tip20,
|
|
109
|
-
functionName: '
|
|
110
|
-
args: [
|
|
99
|
+
functionName: 'transferWithMemo',
|
|
100
|
+
args: [
|
|
101
|
+
'0x0000000000000000000000000000000000000002',
|
|
102
|
+
10n,
|
|
103
|
+
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
104
|
+
],
|
|
111
105
|
}),
|
|
112
106
|
},
|
|
113
107
|
],
|
|
114
108
|
details,
|
|
115
109
|
),
|
|
110
|
+
).not.toThrow()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('error: rejects empty calls', () => {
|
|
114
|
+
expect(() => validateCalls([], details)).toThrow(FeePayerValidationError)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('error: rejects unknown selector', () => {
|
|
118
|
+
expect(() => validateCalls([{ data: '0xdeadbeef' as `0x${string}` }], details)).toThrow(
|
|
119
|
+
'disallowed call pattern',
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('error: rejects more than 11 transfers', () => {
|
|
124
|
+
expect(() =>
|
|
125
|
+
validateCalls(
|
|
126
|
+
Array.from({ length: 12 }, (_, index) => ({
|
|
127
|
+
data: encodeFunctionData({
|
|
128
|
+
abi: Abis.tip20,
|
|
129
|
+
functionName: 'transfer',
|
|
130
|
+
args: [`0x${(index + 1).toString(16).padStart(40, '0')}`, 100n],
|
|
131
|
+
}),
|
|
132
|
+
})),
|
|
133
|
+
details,
|
|
134
|
+
),
|
|
116
135
|
).toThrow('disallowed call pattern')
|
|
117
136
|
})
|
|
118
137
|
|
|
@@ -30,14 +30,29 @@ export function validateCalls(
|
|
|
30
30
|
calls: readonly { data?: `0x${string}` | undefined; to?: TempoAddress.Address | undefined }[],
|
|
31
31
|
details: Record<string, string>,
|
|
32
32
|
) {
|
|
33
|
+
if (calls.length === 0)
|
|
34
|
+
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
35
|
+
|
|
33
36
|
const callSelectors = calls.map((c) => c.data?.slice(0, 10))
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
37
|
+
const hasSwapPrefix = callSelectors[0] === Selectors.approve
|
|
38
|
+
|
|
39
|
+
if (hasSwapPrefix) {
|
|
40
|
+
if (callSelectors[1] !== Selectors.swapExactAmountOut)
|
|
41
|
+
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
42
|
+
} else if (callSelectors[0] === Selectors.swapExactAmountOut) {
|
|
43
|
+
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const transferSelectors = callSelectors.slice(hasSwapPrefix ? 2 : 0)
|
|
47
|
+
if (
|
|
48
|
+
transferSelectors.length === 0 ||
|
|
49
|
+
transferSelectors.length > 11 ||
|
|
50
|
+
transferSelectors.some(
|
|
51
|
+
(selector) => selector !== Selectors.transfer && selector !== Selectors.transferWithMemo,
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
40
54
|
throw new FeePayerValidationError('disallowed call pattern in fee-payer transaction', details)
|
|
55
|
+
}
|
|
41
56
|
|
|
42
57
|
// Validate approve spender and buy target are the DEX.
|
|
43
58
|
const approveCall = calls.find((c) => c.data?.slice(0, 10) === Selectors.approve)
|
|
@@ -650,6 +650,107 @@ describe('tempo', () => {
|
|
|
650
650
|
httpServer.close()
|
|
651
651
|
})
|
|
652
652
|
|
|
653
|
+
test('behavior: accepts split payments', async () => {
|
|
654
|
+
const mppx = Mppx_client.create({
|
|
655
|
+
polyfill: false,
|
|
656
|
+
methods: [
|
|
657
|
+
tempo_client({
|
|
658
|
+
account: accounts[1],
|
|
659
|
+
getClient: () => client,
|
|
660
|
+
}),
|
|
661
|
+
],
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
665
|
+
const result = await Mppx_server.toNodeListener(
|
|
666
|
+
server.charge({
|
|
667
|
+
amount: '1',
|
|
668
|
+
currency: asset,
|
|
669
|
+
recipient: accounts[0].address,
|
|
670
|
+
splits: [
|
|
671
|
+
{ amount: '0.2', recipient: accounts[2].address },
|
|
672
|
+
{
|
|
673
|
+
amount: '0.1',
|
|
674
|
+
memo: '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
|
|
675
|
+
recipient: accounts[3].address,
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
}),
|
|
679
|
+
)(req, res)
|
|
680
|
+
if (result.status === 402) return
|
|
681
|
+
res.end('OK')
|
|
682
|
+
})
|
|
683
|
+
|
|
684
|
+
const response = await mppx.fetch(httpServer.url)
|
|
685
|
+
expect(response.status).toBe(200)
|
|
686
|
+
|
|
687
|
+
httpServer.close()
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
test('behavior: accepts transaction when split transfers are out of order', async () => {
|
|
691
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
692
|
+
const result = await Mppx_server.toNodeListener(
|
|
693
|
+
server.charge({
|
|
694
|
+
amount: '1',
|
|
695
|
+
currency: asset,
|
|
696
|
+
recipient: accounts[0].address,
|
|
697
|
+
splits: [
|
|
698
|
+
{ amount: '0.2', recipient: accounts[2].address },
|
|
699
|
+
{ amount: '0.1', recipient: accounts[3].address },
|
|
700
|
+
],
|
|
701
|
+
}),
|
|
702
|
+
)(req, res)
|
|
703
|
+
if (result.status === 402) return
|
|
704
|
+
res.end('OK')
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
const response = await fetch(httpServer.url)
|
|
708
|
+
expect(response.status).toBe(402)
|
|
709
|
+
|
|
710
|
+
const challenge = Challenge.fromResponse(response, {
|
|
711
|
+
methods: [tempo_client.charge()],
|
|
712
|
+
})
|
|
713
|
+
const splits = challenge.request.methodDetails?.splits ?? []
|
|
714
|
+
const primaryAmount =
|
|
715
|
+
BigInt(challenge.request.amount) - BigInt(splits[0]!.amount) - BigInt(splits[1]!.amount)
|
|
716
|
+
|
|
717
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
718
|
+
account: accounts[1]!,
|
|
719
|
+
calls: [
|
|
720
|
+
Actions.token.transfer.call({
|
|
721
|
+
amount: BigInt(splits[1]!.amount),
|
|
722
|
+
to: splits[1]!.recipient as Hex.Hex,
|
|
723
|
+
token: challenge.request.currency as Hex.Hex,
|
|
724
|
+
}),
|
|
725
|
+
Actions.token.transfer.call({
|
|
726
|
+
amount: primaryAmount,
|
|
727
|
+
to: challenge.request.recipient as Hex.Hex,
|
|
728
|
+
token: challenge.request.currency as Hex.Hex,
|
|
729
|
+
}),
|
|
730
|
+
Actions.token.transfer.call({
|
|
731
|
+
amount: BigInt(splits[0]!.amount),
|
|
732
|
+
to: splits[0]!.recipient as Hex.Hex,
|
|
733
|
+
token: challenge.request.currency as Hex.Hex,
|
|
734
|
+
}),
|
|
735
|
+
],
|
|
736
|
+
nonceKey: 'expiring',
|
|
737
|
+
} as never)
|
|
738
|
+
prepared.gas = prepared.gas! + 5_000n
|
|
739
|
+
const signature = await signTransaction(client, prepared as never)
|
|
740
|
+
|
|
741
|
+
const credential = Credential.from({
|
|
742
|
+
challenge,
|
|
743
|
+
payload: { signature, type: 'transaction' as const },
|
|
744
|
+
})
|
|
745
|
+
|
|
746
|
+
const authResponse = await fetch(httpServer.url, {
|
|
747
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
748
|
+
})
|
|
749
|
+
expect(authResponse.status).toBe(200)
|
|
750
|
+
|
|
751
|
+
httpServer.close()
|
|
752
|
+
})
|
|
753
|
+
|
|
653
754
|
test('behavior: rejects expired request', async () => {
|
|
654
755
|
const httpServer = await Http.createServer(async (req, res) => {
|
|
655
756
|
const result = await Mppx_server.toNodeListener(
|
|
@@ -1156,6 +1257,39 @@ describe('tempo', () => {
|
|
|
1156
1257
|
httpServer.close()
|
|
1157
1258
|
})
|
|
1158
1259
|
|
|
1260
|
+
test('behavior: fee payer with splits', async () => {
|
|
1261
|
+
const mppx = Mppx_client.create({
|
|
1262
|
+
polyfill: false,
|
|
1263
|
+
methods: [
|
|
1264
|
+
tempo_client({
|
|
1265
|
+
account: accounts[1],
|
|
1266
|
+
getClient() {
|
|
1267
|
+
return client
|
|
1268
|
+
},
|
|
1269
|
+
}),
|
|
1270
|
+
],
|
|
1271
|
+
})
|
|
1272
|
+
|
|
1273
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1274
|
+
const result = await Mppx_server.toNodeListener(
|
|
1275
|
+
server.charge({
|
|
1276
|
+
feePayer: accounts[0],
|
|
1277
|
+
amount: '1',
|
|
1278
|
+
currency: asset,
|
|
1279
|
+
recipient: accounts[0].address,
|
|
1280
|
+
splits: [{ amount: '0.2', recipient: accounts[2].address }],
|
|
1281
|
+
}),
|
|
1282
|
+
)(req, res)
|
|
1283
|
+
if (result.status === 402) return
|
|
1284
|
+
res.end('OK')
|
|
1285
|
+
})
|
|
1286
|
+
|
|
1287
|
+
const response = await mppx.fetch(httpServer.url)
|
|
1288
|
+
expect(response.status).toBe(200)
|
|
1289
|
+
|
|
1290
|
+
httpServer.close()
|
|
1291
|
+
})
|
|
1292
|
+
|
|
1159
1293
|
test('behavior: fee payer (hoisted)', async () => {
|
|
1160
1294
|
const mppx = Mppx_client.create({
|
|
1161
1295
|
polyfill: false,
|
|
@@ -1658,6 +1792,70 @@ describe('tempo', () => {
|
|
|
1658
1792
|
|
|
1659
1793
|
httpServer.close()
|
|
1660
1794
|
})
|
|
1795
|
+
|
|
1796
|
+
test('behavior: accepts split transaction when transfers are out of order', async () => {
|
|
1797
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
1798
|
+
const result = await Mppx_server.toNodeListener(
|
|
1799
|
+
server.charge({
|
|
1800
|
+
amount: '1',
|
|
1801
|
+
currency: asset,
|
|
1802
|
+
recipient: accounts[0].address,
|
|
1803
|
+
splits: [
|
|
1804
|
+
{ amount: '0.2', recipient: accounts[2].address },
|
|
1805
|
+
{ amount: '0.1', recipient: accounts[3].address },
|
|
1806
|
+
],
|
|
1807
|
+
}),
|
|
1808
|
+
)(req, res)
|
|
1809
|
+
if (result.status === 402) return
|
|
1810
|
+
res.end('OK')
|
|
1811
|
+
})
|
|
1812
|
+
|
|
1813
|
+
const response = await fetch(httpServer.url)
|
|
1814
|
+
expect(response.status).toBe(402)
|
|
1815
|
+
|
|
1816
|
+
const challenge = Challenge.fromResponse(response, {
|
|
1817
|
+
methods: [tempo_client.charge()],
|
|
1818
|
+
})
|
|
1819
|
+
const splits = challenge.request.methodDetails?.splits ?? []
|
|
1820
|
+
const primaryAmount =
|
|
1821
|
+
BigInt(challenge.request.amount) - BigInt(splits[0]!.amount) - BigInt(splits[1]!.amount)
|
|
1822
|
+
|
|
1823
|
+
const prepared = await prepareTransactionRequest(client, {
|
|
1824
|
+
account: accounts[1]!,
|
|
1825
|
+
calls: [
|
|
1826
|
+
Actions.token.transfer.call({
|
|
1827
|
+
amount: BigInt(splits[0]!.amount),
|
|
1828
|
+
to: splits[0]!.recipient as Hex.Hex,
|
|
1829
|
+
token: challenge.request.currency as Hex.Hex,
|
|
1830
|
+
}),
|
|
1831
|
+
Actions.token.transfer.call({
|
|
1832
|
+
amount: primaryAmount,
|
|
1833
|
+
to: challenge.request.recipient as Hex.Hex,
|
|
1834
|
+
token: challenge.request.currency as Hex.Hex,
|
|
1835
|
+
}),
|
|
1836
|
+
Actions.token.transfer.call({
|
|
1837
|
+
amount: BigInt(splits[1]!.amount),
|
|
1838
|
+
to: splits[1]!.recipient as Hex.Hex,
|
|
1839
|
+
token: challenge.request.currency as Hex.Hex,
|
|
1840
|
+
}),
|
|
1841
|
+
],
|
|
1842
|
+
nonceKey: 'expiring',
|
|
1843
|
+
} as never)
|
|
1844
|
+
prepared.gas = prepared.gas! + 5_000n
|
|
1845
|
+
const signature = await signTransaction(client, prepared as never)
|
|
1846
|
+
|
|
1847
|
+
const credential = Credential.from({
|
|
1848
|
+
challenge,
|
|
1849
|
+
payload: { signature, type: 'transaction' as const },
|
|
1850
|
+
})
|
|
1851
|
+
|
|
1852
|
+
const authResponse = await fetch(httpServer.url, {
|
|
1853
|
+
headers: { Authorization: Credential.serialize(credential) },
|
|
1854
|
+
})
|
|
1855
|
+
expect(authResponse.status).toBe(200)
|
|
1856
|
+
|
|
1857
|
+
httpServer.close()
|
|
1858
|
+
})
|
|
1661
1859
|
})
|
|
1662
1860
|
|
|
1663
1861
|
describe('intent: charge; type: transaction; waitForConfirmation: false', () => {
|
|
@@ -2295,6 +2493,39 @@ describe('tempo', () => {
|
|
|
2295
2493
|
httpServer.close()
|
|
2296
2494
|
})
|
|
2297
2495
|
|
|
2496
|
+
test('swaps via DEX when user lacks target currency for split payments', async () => {
|
|
2497
|
+
const mppx = Mppx_client.create({
|
|
2498
|
+
polyfill: false,
|
|
2499
|
+
methods: [
|
|
2500
|
+
tempo_client({
|
|
2501
|
+
account: swapPayer,
|
|
2502
|
+
autoSwap: true,
|
|
2503
|
+
getClient() {
|
|
2504
|
+
return client
|
|
2505
|
+
},
|
|
2506
|
+
}),
|
|
2507
|
+
],
|
|
2508
|
+
})
|
|
2509
|
+
|
|
2510
|
+
const httpServer = await Http.createServer(async (req, res) => {
|
|
2511
|
+
const result = await Mppx_server.toNodeListener(
|
|
2512
|
+
server.charge({
|
|
2513
|
+
amount: '1',
|
|
2514
|
+
currency: asset,
|
|
2515
|
+
recipient: accounts[0]!.address,
|
|
2516
|
+
splits: [{ amount: '0.2', recipient: accounts[2]!.address }],
|
|
2517
|
+
}),
|
|
2518
|
+
)(req, res)
|
|
2519
|
+
if (result.status === 402) return
|
|
2520
|
+
res.end('OK')
|
|
2521
|
+
})
|
|
2522
|
+
|
|
2523
|
+
const response = await mppx.fetch(httpServer.url)
|
|
2524
|
+
expect(response.status).toBe(200)
|
|
2525
|
+
|
|
2526
|
+
httpServer.close()
|
|
2527
|
+
})
|
|
2528
|
+
|
|
2298
2529
|
test('custom slippage and tokenIn', async () => {
|
|
2299
2530
|
const mppx = Mppx_client.create({
|
|
2300
2531
|
polyfill: false,
|