tapimo 0.7.2 → 0.8.0
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/README.md +10 -7
- package/dist/ApiKey.d.ts +3 -0
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +10 -0
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +7 -3
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +9 -2
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +20434 -19484
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +62 -4
- package/dist/App.js.map +1 -1
- package/dist/admin/App.d.ts +84 -74
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +14 -1
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -2
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +2 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +168 -168
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +67 -67
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +2 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +3 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/data/App.d.ts +10305 -12785
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +4 -8
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +24 -24
- package/dist/apps/data/routes/addresses.d.ts +288 -288
- package/dist/apps/data/routes/balances.d.ts +6 -6
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +170 -170
- package/dist/apps/data/routes/earn.d.ts +436 -6
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +631 -4
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +10 -10
- package/dist/apps/data/routes/fee-amm.d.ts +2 -2
- package/dist/apps/data/routes/indexer.d.ts +6 -6
- package/dist/apps/data/routes/indexer.js +4 -1
- package/dist/apps/data/routes/indexer.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts +235 -235
- package/dist/apps/data/routes/rpc.d.ts +1 -1
- package/dist/apps/data/routes/rpc.js +4 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +36 -36
- package/dist/apps/data/routes/tokens.d.ts +6 -6
- package/dist/apps/data/routes/transactions.d.ts +2 -2
- package/dist/apps/data/routes/transfers.d.ts +1 -1
- package/dist/apps/data/routes/valuation.d.ts +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
- package/dist/apps/data/routes/webhooks.d.ts +9 -9
- package/dist/apps/data/routes/zones.d.ts +14 -14
- package/dist/apps/funding/Access.d.ts +16 -0
- package/dist/apps/funding/Access.d.ts.map +1 -0
- package/dist/apps/funding/Access.js +22 -0
- package/dist/apps/funding/Access.js.map +1 -0
- package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1717 -3035
- package/dist/apps/funding/App.d.ts.map +1 -0
- package/dist/apps/funding/App.js +46 -0
- package/dist/apps/funding/App.js.map +1 -0
- package/dist/apps/funding/Inventory.d.ts +5 -0
- package/dist/apps/funding/Inventory.d.ts.map +1 -0
- package/dist/apps/funding/Inventory.js +25 -0
- package/dist/apps/funding/Inventory.js.map +1 -0
- package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
- package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
- package/dist/apps/funding/ProviderAttempts.js +24 -0
- package/dist/apps/funding/ProviderAttempts.js.map +1 -0
- package/dist/apps/funding/index.d.ts +2 -0
- package/dist/apps/funding/index.d.ts.map +1 -0
- package/dist/apps/funding/index.js +2 -0
- package/dist/apps/funding/index.js.map +1 -0
- package/dist/apps/funding/routes/chains.d.ts +169 -0
- package/dist/apps/funding/routes/chains.d.ts.map +1 -0
- package/dist/apps/funding/routes/chains.js +80 -0
- package/dist/apps/funding/routes/chains.js.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts +2388 -0
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
- package/dist/apps/funding/routes/deposit-addresses.js +860 -0
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
- package/dist/apps/funding/routes/providers.d.ts +91 -0
- package/dist/apps/funding/routes/providers.d.ts.map +1 -0
- package/dist/apps/funding/routes/providers.js +56 -0
- package/dist/apps/funding/routes/providers.js.map +1 -0
- package/dist/apps/funding/routes/quotes.d.ts +426 -0
- package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
- package/dist/apps/funding/routes/quotes.js +188 -0
- package/dist/apps/funding/routes/quotes.js.map +1 -0
- package/dist/apps/funding/routes/transfers.d.ts +1999 -0
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
- package/dist/apps/funding/routes/transfers.js +614 -0
- package/dist/apps/funding/routes/transfers.js.map +1 -0
- package/dist/apps/index.d.ts +1 -0
- package/dist/apps/index.d.ts.map +1 -1
- package/dist/apps/index.js +1 -0
- package/dist/apps/index.js.map +1 -1
- package/dist/apps/management/App.d.ts +430 -165
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +298 -21
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +75 -1
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +6 -6
- package/dist/apps/management/routes/faucet.d.ts +35 -35
- package/dist/apps/management/routes/invitations.d.ts +48 -48
- package/dist/apps/management/routes/invite-links.d.ts +38 -38
- package/dist/apps/management/routes/members.d.ts +69 -69
- package/dist/apps/management/routes/orgs.d.ts +37 -37
- package/dist/apps/management/routes/projects.d.ts +25 -25
- package/dist/apps/management/routes/scopes.d.ts +1 -1
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +18 -18
- package/dist/apps/management/routes/webhooks.d.ts +128 -128
- package/dist/apps/mcp.d.ts +2 -3
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +16 -14
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/metadata.d.ts +4 -0
- package/dist/apps/metadata.d.ts.map +1 -1
- package/dist/apps/metadata.js +10 -0
- package/dist/apps/metadata.js.map +1 -1
- package/dist/apps/mpp/App.d.ts +2 -2
- package/dist/cloudflare.d.ts +50 -0
- package/dist/cloudflare.d.ts.map +1 -1
- package/dist/cloudflare.js +178 -0
- package/dist/cloudflare.js.map +1 -1
- package/dist/db/Db.d.ts +2 -0
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +37 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +12 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +20 -0
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
- package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
- package/dist/db/tables/fundingDepositAddresses.js +12 -0
- package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
- package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
- package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
- package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
- package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
- package/dist/db/tables/fundingDeposits.d.ts +57 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +82 -0
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/internal/Auth.d.ts +11 -11
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +41 -15
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cache.js +1 -1
- package/dist/internal/Cache.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts +2 -2
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +10 -4
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/IpAllowlist.d.ts +15 -0
- package/dist/internal/IpAllowlist.d.ts.map +1 -0
- package/dist/internal/IpAllowlist.js +27 -0
- package/dist/internal/IpAllowlist.js.map +1 -0
- package/dist/internal/OpenApi.d.ts +1 -1
- package/dist/internal/OpenApi.js +1 -1
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Tidx.d.ts +6 -0
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +13 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts +8 -0
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +12 -5
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/funding/Deposit.d.ts +42 -0
- package/dist/internal/funding/Deposit.d.ts.map +1 -1
- package/dist/internal/funding/Deposit.js +37 -5
- package/dist/internal/funding/Deposit.js.map +1 -1
- package/dist/internal/funding/Evidence.d.ts +1 -5
- package/dist/internal/funding/Evidence.d.ts.map +1 -1
- package/dist/internal/funding/Evidence.js +4 -3
- package/dist/internal/funding/Evidence.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +102 -0
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +5 -0
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts +16 -2
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +257 -48
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/SourceObservation.d.ts +108 -0
- package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
- package/dist/internal/funding/SourceObservation.js +143 -0
- package/dist/internal/funding/SourceObservation.js.map +1 -0
- package/dist/internal/funding/Subsidy.d.ts +2 -2
- package/dist/internal/funding/Subsidy.d.ts.map +1 -1
- package/dist/internal/funding/Subsidy.js +1 -1
- package/dist/internal/funding/Subsidy.js.map +1 -1
- package/dist/internal/funding/index.d.ts +1 -0
- package/dist/internal/funding/index.d.ts.map +1 -1
- package/dist/internal/funding/index.js +1 -0
- package/dist/internal/funding/index.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +43 -1
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +434 -27
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/ApiKey.test.ts +35 -0
- package/src/ApiKey.ts +27 -0
- package/src/ApiKeys.test.ts +29 -0
- package/src/ApiKeys.ts +11 -3
- package/src/App.test.ts +23 -5
- package/src/App.ts +76 -5
- package/src/Client.test-d.ts +171 -19
- package/src/admin/apps/api-keys.test.ts +25 -1
- package/src/admin/apps/api-keys.ts +6 -2
- package/src/admin/ui.generated.ts +67 -67
- package/src/analytics/Analytics.ts +4 -0
- package/src/apps/composition.test.ts +100 -2
- package/src/apps/data/App.test.ts +0 -12
- package/src/apps/data/App.ts +4 -8
- package/src/apps/data/routes/earn.test.ts +283 -0
- package/src/apps/data/routes/earn.ts +800 -3
- package/src/apps/data/routes/indexer.ts +2 -1
- package/src/apps/data/routes/rpc.ts +2 -0
- package/src/apps/data/routes/tokens.test.ts +6 -8
- package/src/apps/funding/Access.ts +26 -0
- package/src/apps/funding/App.test.ts +78 -0
- package/src/apps/funding/App.ts +63 -0
- package/src/apps/funding/Inventory.ts +30 -0
- package/src/apps/funding/ProviderAttempts.ts +26 -0
- package/src/apps/funding/index.ts +1 -0
- package/src/apps/funding/routes/chains.test.ts +78 -0
- package/src/apps/funding/routes/chains.ts +101 -0
- package/src/apps/funding/routes/deposit-addresses.test.ts +1628 -0
- package/src/apps/funding/routes/deposit-addresses.ts +1015 -0
- package/src/apps/funding/routes/providers.test.ts +165 -0
- package/src/apps/funding/routes/providers.ts +73 -0
- package/src/apps/funding/routes/quotes.test.ts +511 -0
- package/src/apps/funding/routes/quotes.ts +205 -0
- package/src/apps/funding/routes/transfers.test.ts +1255 -0
- package/src/apps/funding/routes/transfers.ts +704 -0
- package/src/apps/index.ts +1 -0
- package/src/apps/management/routes/api-keys.test.ts +98 -0
- package/src/apps/management/routes/api-keys.ts +96 -1
- package/src/apps/mcp.test.ts +17 -2
- package/src/apps/mcp.ts +18 -13
- package/src/apps/metadata.ts +12 -0
- package/src/apps/mpp/internal/Policy.test.ts +7 -1
- package/src/cloudflare.ts +279 -0
- package/src/db/Db.ts +62 -0
- package/src/db/Schema.ts +24 -0
- package/src/db/tables/fundingDepositAddresses.ts +28 -0
- package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
- package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
- package/src/db/tables/fundingDeposits.test.ts +138 -0
- package/src/db/tables/fundingDeposits.ts +145 -1
- package/src/internal/Auth.test.ts +177 -5
- package/src/internal/Auth.ts +48 -18
- package/src/internal/Cache.test.ts +37 -3
- package/src/internal/Cache.ts +1 -1
- package/src/internal/EdgeCache.test.ts +7 -7
- package/src/internal/EdgeCache.ts +13 -4
- package/src/internal/IpAllowlist.test.ts +52 -0
- package/src/internal/IpAllowlist.ts +39 -0
- package/src/internal/Log.test.ts +7 -1
- package/src/internal/OpenApi.ts +1 -1
- package/src/internal/Tidx.test.ts +28 -0
- package/src/internal/Tidx.ts +19 -3
- package/src/internal/Viem.test.ts +28 -0
- package/src/internal/Viem.ts +21 -5
- package/src/internal/funding/Deposit.ts +74 -5
- package/src/internal/funding/Evidence.test.ts +2 -2
- package/src/internal/funding/Evidence.ts +4 -9
- package/src/internal/funding/Provider.ts +115 -0
- package/src/internal/funding/Reconciliation.local.test.ts +7 -0
- package/src/internal/funding/Reconciliation.test.ts +1052 -35
- package/src/internal/funding/Reconciliation.ts +328 -56
- package/src/internal/funding/SourceObservation.test.ts +301 -0
- package/src/internal/funding/SourceObservation.ts +267 -0
- package/src/internal/funding/Subsidy.ts +2 -2
- package/src/internal/funding/index.test.ts +1 -0
- package/src/internal/funding/index.ts +1 -0
- package/src/internal/funding/providers/relay.test.ts +94 -4
- package/src/internal/funding/providers/relay.ts +528 -27
- package/dist/apps/data/routes/funding.d.ts.map +0 -1
- package/dist/apps/data/routes/funding.js +0 -1557
- package/dist/apps/data/routes/funding.js.map +0 -1
- package/src/apps/data/routes/funding.test.ts +0 -2994
- package/src/apps/data/routes/funding.ts +0 -1821
|
@@ -0,0 +1,1628 @@
|
|
|
1
|
+
import { Hash, Hex } from 'ox'
|
|
2
|
+
|
|
3
|
+
import { tempoMainnet } from 'viem/tempo/chains'
|
|
4
|
+
|
|
5
|
+
import * as FundingDepositAddresses from '../../../db/tables/fundingDepositAddresses.js'
|
|
6
|
+
|
|
7
|
+
import * as FundingIdempotencyTable from '../../../db/tables/fundingIdempotency.js'
|
|
8
|
+
|
|
9
|
+
import * as Cache from '../../../internal/Cache.js'
|
|
10
|
+
|
|
11
|
+
import * as Cursor from '../../../internal/Cursor.js'
|
|
12
|
+
|
|
13
|
+
import * as FundingDeposit from '../../../internal/funding/Deposit.js'
|
|
14
|
+
|
|
15
|
+
import * as FundingDepositAddress from '../../../internal/funding/DepositAddress.js'
|
|
16
|
+
|
|
17
|
+
import * as FundingProvider from '../../../internal/funding/Provider.js'
|
|
18
|
+
|
|
19
|
+
import * as Idempotency from '../../../internal/Idempotency.js'
|
|
20
|
+
|
|
21
|
+
import * as Across from '../../../internal/funding/providers/across.js'
|
|
22
|
+
|
|
23
|
+
import * as Relay from '../../../internal/funding/providers/relay.js'
|
|
24
|
+
|
|
25
|
+
import * as Rhino from '../../../internal/funding/providers/rhino.js'
|
|
26
|
+
|
|
27
|
+
import * as Squid from '../../../internal/funding/providers/squid.js'
|
|
28
|
+
|
|
29
|
+
import * as Symbiosis from '../../../internal/funding/providers/symbiosis.js'
|
|
30
|
+
|
|
31
|
+
import type * as Log from '../../../internal/Log.js'
|
|
32
|
+
|
|
33
|
+
import * as TestApp from '../../../../test/App.js'
|
|
34
|
+
|
|
35
|
+
import * as TestFunding from '../../../../test/Funding.js'
|
|
36
|
+
|
|
37
|
+
import * as Funding from './deposit-addresses.js'
|
|
38
|
+
|
|
39
|
+
const db = TestApp.database()
|
|
40
|
+
const relayApiKey = process.env.RELAY_API_KEY
|
|
41
|
+
|
|
42
|
+
const providers = [
|
|
43
|
+
Across.across({
|
|
44
|
+
apiKey: 'across-api-key',
|
|
45
|
+
integratorId: '0xdead',
|
|
46
|
+
userAddress: '0x1111111111111111111111111111111111111111',
|
|
47
|
+
}),
|
|
48
|
+
Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
|
|
49
|
+
Rhino.rhino(),
|
|
50
|
+
Squid.squid({
|
|
51
|
+
integratorId: 'squid-integrator-id',
|
|
52
|
+
userAddress: '0x1111111111111111111111111111111111111111',
|
|
53
|
+
}),
|
|
54
|
+
Symbiosis.symbiosis({ userAddress: '0x1111111111111111111111111111111111111111' }),
|
|
55
|
+
] as const
|
|
56
|
+
|
|
57
|
+
beforeAll(() => TestFunding.publish(db))
|
|
58
|
+
|
|
59
|
+
function create(options: TestApp.create.Options = {}) {
|
|
60
|
+
return TestApp.create({ ...options, db })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe('POST /funding/deposit-addresses', () => {
|
|
64
|
+
test('requires funding:write and an idempotency key', async () => {
|
|
65
|
+
const app = transfersApp({ providers: [depositAddressProvider()] })
|
|
66
|
+
expect((await postDepositAddress(app, { headers: {} })).status).toBe(401)
|
|
67
|
+
expect((await postDepositAddress(app, { key: fundingReader })).status).toBe(403)
|
|
68
|
+
const response = await postDepositAddress(app, {
|
|
69
|
+
headers: { authorization: `Bearer ${fundingWriter.token}` },
|
|
70
|
+
})
|
|
71
|
+
expect(response.status).toBe(400)
|
|
72
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
73
|
+
'idempotency_key_required',
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('requires a source amount', async () => {
|
|
78
|
+
const { amount: _amount, ...body } = depositAddressBody
|
|
79
|
+
const response = await postDepositAddress(
|
|
80
|
+
transfersApp({ providers: [depositAddressProvider()] }),
|
|
81
|
+
{ body },
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
|
|
85
|
+
{
|
|
86
|
+
body: { requestId: expect.any(String) },
|
|
87
|
+
},
|
|
88
|
+
`
|
|
89
|
+
{
|
|
90
|
+
"body": {
|
|
91
|
+
"error": {
|
|
92
|
+
"code": "body_invalid",
|
|
93
|
+
"details": [
|
|
94
|
+
{
|
|
95
|
+
"message": "Invalid input: expected string, received undefined",
|
|
96
|
+
"path": [
|
|
97
|
+
"amount",
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
"message": "Check the request body and try again.",
|
|
102
|
+
},
|
|
103
|
+
"requestId": Any<String>,
|
|
104
|
+
},
|
|
105
|
+
"status": 400,
|
|
106
|
+
}
|
|
107
|
+
`,
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('creates a reusable address and persists private provider correlation', async () => {
|
|
112
|
+
let seen: FundingProvider.createDepositAddress.Parameters | undefined
|
|
113
|
+
const registered: FundingDepositAddress.Public[] = []
|
|
114
|
+
const app = transfersApp({
|
|
115
|
+
funding: {
|
|
116
|
+
registerDepositAddress: async (address) => {
|
|
117
|
+
registered.push(address)
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
providers: [
|
|
121
|
+
depositAddressProvider('TBXSw8fM4jpQkGc6zZjsVABFpVN7UvXPdV', (input) => {
|
|
122
|
+
seen = input
|
|
123
|
+
}),
|
|
124
|
+
],
|
|
125
|
+
})
|
|
126
|
+
const response = await postDepositAddress(app, {
|
|
127
|
+
body: { ...depositAddressBody, destinationToken: 'usdt0', sourceToken: 'usdt' },
|
|
128
|
+
})
|
|
129
|
+
expect(response.status).toBe(200)
|
|
130
|
+
expect(response.headers.get('cache-control')).toBe('no-store')
|
|
131
|
+
const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
132
|
+
expect(body).toMatchObject({
|
|
133
|
+
address: 'TBXSw8fM4jpQkGc6zZjsVABFpVN7UvXPdV',
|
|
134
|
+
destinationToken: { symbol: 'USDT0' },
|
|
135
|
+
provider: { id: 'relay', name: 'Relay' },
|
|
136
|
+
recipient: depositAddressBody.recipient,
|
|
137
|
+
refundAddress: depositAddressBody.refundAddress,
|
|
138
|
+
sourceChain: { id: 'tron:0x2b6653dc' },
|
|
139
|
+
sourceToken: { symbol: 'USDT' },
|
|
140
|
+
status: 'active',
|
|
141
|
+
})
|
|
142
|
+
expect(seen).toMatchObject({
|
|
143
|
+
recipient: depositAddressBody.recipient,
|
|
144
|
+
refundAddress: depositAddressBody.refundAddress,
|
|
145
|
+
})
|
|
146
|
+
const record = await FundingDepositAddresses.get(db, body.id)
|
|
147
|
+
expect(record).toMatchObject({
|
|
148
|
+
deliveryStrategy: 'provider',
|
|
149
|
+
providerOutputToken: body.destinationToken,
|
|
150
|
+
providerRequestIds: ['relay_request_1'],
|
|
151
|
+
providerState: { checkEndpoint: '/intents/status/v3?requestId=relay_request_1' },
|
|
152
|
+
})
|
|
153
|
+
expect(registered).toMatchInlineSnapshot(
|
|
154
|
+
[
|
|
155
|
+
{
|
|
156
|
+
createdAt: expect.any(String),
|
|
157
|
+
id: expect.any(String),
|
|
158
|
+
updatedAt: expect.any(String),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
`
|
|
162
|
+
[
|
|
163
|
+
{
|
|
164
|
+
"address": "TBXSw8fM4jpQkGc6zZjsVABFpVN7UvXPdV",
|
|
165
|
+
"createdAt": Any<String>,
|
|
166
|
+
"destinationChain": {
|
|
167
|
+
"addressFormat": "hex",
|
|
168
|
+
"id": "eip155:4217",
|
|
169
|
+
"kind": "evm",
|
|
170
|
+
"name": "Tempo",
|
|
171
|
+
},
|
|
172
|
+
"destinationToken": {
|
|
173
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
174
|
+
"currency": "USD",
|
|
175
|
+
"decimals": 6,
|
|
176
|
+
"name": "USDT0",
|
|
177
|
+
"standard": "TIP-20",
|
|
178
|
+
"symbol": "USDT0",
|
|
179
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
180
|
+
"verified": true,
|
|
181
|
+
},
|
|
182
|
+
"id": Any<String>,
|
|
183
|
+
"provider": {
|
|
184
|
+
"id": "relay",
|
|
185
|
+
"name": "Relay",
|
|
186
|
+
},
|
|
187
|
+
"recipient": "0x1111111111111111111111111111111111111111",
|
|
188
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
189
|
+
"sourceChain": {
|
|
190
|
+
"addressFormat": "base58check",
|
|
191
|
+
"id": "tron:0x2b6653dc",
|
|
192
|
+
"kind": "tron",
|
|
193
|
+
"name": "Tron",
|
|
194
|
+
},
|
|
195
|
+
"sourceToken": {
|
|
196
|
+
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
197
|
+
"currency": "USD",
|
|
198
|
+
"decimals": 6,
|
|
199
|
+
"name": "Tether USD",
|
|
200
|
+
"standard": "TRC-20",
|
|
201
|
+
"symbol": "USDT",
|
|
202
|
+
"tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
203
|
+
"verified": true,
|
|
204
|
+
},
|
|
205
|
+
"status": "active",
|
|
206
|
+
"subsidize": false,
|
|
207
|
+
"updatedAt": Any<String>,
|
|
208
|
+
},
|
|
209
|
+
]
|
|
210
|
+
`,
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('resolves token identifiers through the composed route', async () => {
|
|
215
|
+
const app = transfersApp({
|
|
216
|
+
providers: [depositAddressProvider('T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb')],
|
|
217
|
+
})
|
|
218
|
+
const identifiers = [
|
|
219
|
+
{ destinationToken: 'usdt0', name: 'alias', sourceToken: 'usdt' },
|
|
220
|
+
{
|
|
221
|
+
destinationToken: depositAddressBody.destinationToken,
|
|
222
|
+
name: 'address',
|
|
223
|
+
sourceToken: depositAddressBody.sourceToken,
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
destinationToken: 'eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73',
|
|
227
|
+
name: 'token key',
|
|
228
|
+
sourceToken: 'tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
|
229
|
+
},
|
|
230
|
+
] as const
|
|
231
|
+
const results = []
|
|
232
|
+
for (const identifier of identifiers) {
|
|
233
|
+
const response = await postDepositAddress(app, {
|
|
234
|
+
body: {
|
|
235
|
+
...depositAddressBody,
|
|
236
|
+
destinationToken: identifier.destinationToken,
|
|
237
|
+
recipient: `0x${'55'.repeat(20)}`,
|
|
238
|
+
refundAddress: 'TD5gsCwxykWsLN9aPrq2TAfNjByuZKYp4E',
|
|
239
|
+
sourceToken: identifier.sourceToken,
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
243
|
+
results.push({
|
|
244
|
+
destinationToken: body.destinationToken.symbol,
|
|
245
|
+
name: identifier.name,
|
|
246
|
+
sourceToken: body.sourceToken.symbol,
|
|
247
|
+
status: response.status,
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
expect(results).toMatchInlineSnapshot(`
|
|
252
|
+
[
|
|
253
|
+
{
|
|
254
|
+
"destinationToken": "USDT0",
|
|
255
|
+
"name": "alias",
|
|
256
|
+
"sourceToken": "USDT",
|
|
257
|
+
"status": 200,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"destinationToken": "USDT0",
|
|
261
|
+
"name": "address",
|
|
262
|
+
"sourceToken": "USDT",
|
|
263
|
+
"status": 200,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
"destinationToken": "USDT0",
|
|
267
|
+
"name": "token key",
|
|
268
|
+
"sourceToken": "USDT",
|
|
269
|
+
"status": 200,
|
|
270
|
+
},
|
|
271
|
+
]
|
|
272
|
+
`)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
test('omits newly quoted terms from a matching reusable address', async () => {
|
|
276
|
+
let calls = 0
|
|
277
|
+
const body = { ...depositAddressBody, refundAddress: 'TD5gsCwxykWsLN9aPrq2TAfNjByuZKYp4E' }
|
|
278
|
+
const app = transfersApp({
|
|
279
|
+
providers: [
|
|
280
|
+
depositAddressProvider('TEdvoHEatmDKvTh3o9vBRB9Vdtbhn4QFhy', () => {
|
|
281
|
+
calls += 1
|
|
282
|
+
}),
|
|
283
|
+
],
|
|
284
|
+
})
|
|
285
|
+
const first = await TestApp.json(
|
|
286
|
+
await postDepositAddress(app, { body, idempotencyKey: nextDepositAddressKey() }),
|
|
287
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
288
|
+
)
|
|
289
|
+
const second = await TestApp.json(
|
|
290
|
+
await postDepositAddress(app, {
|
|
291
|
+
body: { ...body, amount: '200000000' },
|
|
292
|
+
idempotencyKey: nextDepositAddressKey(),
|
|
293
|
+
}),
|
|
294
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
295
|
+
)
|
|
296
|
+
if (!('sourceAmount' in first)) throw new Error('Expected newly provisioned terms.')
|
|
297
|
+
|
|
298
|
+
expect({
|
|
299
|
+
calls,
|
|
300
|
+
firstHasQuote: 'quote' in first,
|
|
301
|
+
firstId: first.id,
|
|
302
|
+
firstSourceAmount: first.sourceAmount.baseUnits,
|
|
303
|
+
secondHasQuote: 'quote' in second,
|
|
304
|
+
secondId: second.id,
|
|
305
|
+
}).toMatchInlineSnapshot(
|
|
306
|
+
{
|
|
307
|
+
firstId: expect.any(String),
|
|
308
|
+
secondId: expect.any(String),
|
|
309
|
+
},
|
|
310
|
+
`
|
|
311
|
+
{
|
|
312
|
+
"calls": 2,
|
|
313
|
+
"firstHasQuote": true,
|
|
314
|
+
"firstId": Any<String>,
|
|
315
|
+
"firstSourceAmount": "100000000",
|
|
316
|
+
"secondHasQuote": false,
|
|
317
|
+
"secondId": Any<String>,
|
|
318
|
+
}
|
|
319
|
+
`,
|
|
320
|
+
)
|
|
321
|
+
expect(second.id).toBe(first.id)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('stores subsidized addresses as a separate reusable identity', async () => {
|
|
325
|
+
const inputs: FundingProvider.createDepositAddress.Parameters[] = []
|
|
326
|
+
const app = transfersApp({
|
|
327
|
+
providers: [
|
|
328
|
+
withCreateDepositAddress('relay', async (input) => {
|
|
329
|
+
inputs.push(input)
|
|
330
|
+
return {
|
|
331
|
+
address: inputs.length === 1 ? `0x${'31'.repeat(20)}` : `0x${'32'.repeat(20)}`,
|
|
332
|
+
correlation: { requestId: `relay_request_${inputs.length}` },
|
|
333
|
+
destinationAmount: '1000000',
|
|
334
|
+
destinationAmountMin: '1000000',
|
|
335
|
+
fees: [],
|
|
336
|
+
sampledAt: '2026-08-04T00:00:00.000Z',
|
|
337
|
+
}
|
|
338
|
+
}),
|
|
339
|
+
],
|
|
340
|
+
})
|
|
341
|
+
const body = {
|
|
342
|
+
...depositAddressBody,
|
|
343
|
+
refundAddress: `0x${'33'.repeat(20)}`,
|
|
344
|
+
sourceChain: 'base',
|
|
345
|
+
sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
|
|
346
|
+
}
|
|
347
|
+
const standard = await TestApp.json(
|
|
348
|
+
await postDepositAddress(app, { body }),
|
|
349
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
350
|
+
)
|
|
351
|
+
const subsidized = await TestApp.json(
|
|
352
|
+
await postDepositAddress(app, { body: { ...body, subsidize: true } }),
|
|
353
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
expect({
|
|
357
|
+
distinct: standard.id !== subsidized.id,
|
|
358
|
+
inputs: inputs.map(({ subsidize }) => subsidize),
|
|
359
|
+
standard: standard.subsidize,
|
|
360
|
+
subsidized: subsidized.subsidize,
|
|
361
|
+
}).toMatchInlineSnapshot(`
|
|
362
|
+
{
|
|
363
|
+
"distinct": true,
|
|
364
|
+
"inputs": [
|
|
365
|
+
false,
|
|
366
|
+
true,
|
|
367
|
+
],
|
|
368
|
+
"standard": false,
|
|
369
|
+
"subsidized": true,
|
|
370
|
+
}
|
|
371
|
+
`)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('normalizes EVM accounts before provisioning and matching', async () => {
|
|
375
|
+
const inputs: FundingProvider.createDepositAddress.Parameters[] = []
|
|
376
|
+
const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
|
|
377
|
+
const refundAddress = '0x1234567890AbcdEF1234567890aBcdef12345678'
|
|
378
|
+
const body = {
|
|
379
|
+
...depositAddressBody,
|
|
380
|
+
recipient,
|
|
381
|
+
refundAddress,
|
|
382
|
+
sourceChain: 'base',
|
|
383
|
+
sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
|
|
384
|
+
}
|
|
385
|
+
const app = transfersApp({
|
|
386
|
+
providers: [
|
|
387
|
+
depositAddressProvider(`0x${'99'.repeat(20)}`, (input) => {
|
|
388
|
+
inputs.push(input)
|
|
389
|
+
}),
|
|
390
|
+
],
|
|
391
|
+
})
|
|
392
|
+
const first = await TestApp.json(
|
|
393
|
+
await postDepositAddress(app, { body, idempotencyKey: nextDepositAddressKey() }),
|
|
394
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
395
|
+
)
|
|
396
|
+
const second = await TestApp.json(
|
|
397
|
+
await postDepositAddress(app, {
|
|
398
|
+
body: {
|
|
399
|
+
...body,
|
|
400
|
+
recipient: recipient.toLowerCase(),
|
|
401
|
+
refundAddress: refundAddress.toLowerCase(),
|
|
402
|
+
},
|
|
403
|
+
idempotencyKey: nextDepositAddressKey(),
|
|
404
|
+
}),
|
|
405
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
expect({
|
|
409
|
+
first: { recipient: first.recipient, refundAddress: first.refundAddress },
|
|
410
|
+
inputs: inputs.map(({ recipient, refundAddress }) => ({ recipient, refundAddress })),
|
|
411
|
+
reused: first.id === second.id,
|
|
412
|
+
second: { recipient: second.recipient, refundAddress: second.refundAddress },
|
|
413
|
+
}).toMatchInlineSnapshot(`
|
|
414
|
+
{
|
|
415
|
+
"first": {
|
|
416
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
417
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
418
|
+
},
|
|
419
|
+
"inputs": [
|
|
420
|
+
{
|
|
421
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
422
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
426
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
"reused": true,
|
|
430
|
+
"second": {
|
|
431
|
+
"recipient": "0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD",
|
|
432
|
+
"refundAddress": "0x1234567890AbcdEF1234567890aBcdef12345678",
|
|
433
|
+
},
|
|
434
|
+
}
|
|
435
|
+
`)
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
test('keeps the stored reusable address when the provider rotates addresses', async () => {
|
|
439
|
+
const body = { ...depositAddressBody, recipient: `0x${'22'.repeat(20)}` }
|
|
440
|
+
const first = await TestApp.json(
|
|
441
|
+
await postDepositAddress(
|
|
442
|
+
transfersApp({
|
|
443
|
+
providers: [depositAddressProvider('TGCAjMXComunWZEXCT1LPBdcYbDVuyexBv')],
|
|
444
|
+
}),
|
|
445
|
+
{ body, idempotencyKey: nextDepositAddressKey() },
|
|
446
|
+
),
|
|
447
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
let calls = 0
|
|
451
|
+
const app = transfersApp({
|
|
452
|
+
providers: [
|
|
453
|
+
depositAddressProvider('THkQfRopincF6emzbk6VMC7jTHqJ8MP8g7', () => {
|
|
454
|
+
calls += 1
|
|
455
|
+
}),
|
|
456
|
+
],
|
|
457
|
+
})
|
|
458
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
459
|
+
const response = await postDepositAddress(app, {
|
|
460
|
+
body: { ...body, amount: '200000000' },
|
|
461
|
+
idempotencyKey,
|
|
462
|
+
})
|
|
463
|
+
const second = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
464
|
+
|
|
465
|
+
expect({
|
|
466
|
+
address: second.address,
|
|
467
|
+
calls,
|
|
468
|
+
hasQuote: 'quote' in second,
|
|
469
|
+
reused: second.id === first.id,
|
|
470
|
+
status: response.status,
|
|
471
|
+
}).toMatchInlineSnapshot(
|
|
472
|
+
{
|
|
473
|
+
address: expect.any(String),
|
|
474
|
+
},
|
|
475
|
+
`
|
|
476
|
+
{
|
|
477
|
+
"address": Any<String>,
|
|
478
|
+
"calls": 1,
|
|
479
|
+
"hasQuote": false,
|
|
480
|
+
"reused": true,
|
|
481
|
+
"status": 200,
|
|
482
|
+
}
|
|
483
|
+
`,
|
|
484
|
+
)
|
|
485
|
+
expect(second.address).toBe(first.address)
|
|
486
|
+
expect(
|
|
487
|
+
await TestApp.json(
|
|
488
|
+
await postDepositAddress(app, {
|
|
489
|
+
body: { ...body, amount: '200000000' },
|
|
490
|
+
idempotencyKey,
|
|
491
|
+
}),
|
|
492
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
493
|
+
),
|
|
494
|
+
).toEqual(second)
|
|
495
|
+
expect(calls).toBe(1)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
test('rejects a refund address from another chain', async () => {
|
|
499
|
+
const response = await postDepositAddress(
|
|
500
|
+
transfersApp({ providers: [depositAddressProvider()] }),
|
|
501
|
+
{ body: { ...depositAddressBody, refundAddress: `0x${'11'.repeat(20)}` } },
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
|
|
505
|
+
{
|
|
506
|
+
body: { requestId: expect.any(String) },
|
|
507
|
+
},
|
|
508
|
+
`
|
|
509
|
+
{
|
|
510
|
+
"body": {
|
|
511
|
+
"error": {
|
|
512
|
+
"code": "body_invalid",
|
|
513
|
+
"message": "Invalid \`refundAddress\`.",
|
|
514
|
+
},
|
|
515
|
+
"requestId": Any<String>,
|
|
516
|
+
},
|
|
517
|
+
"status": 400,
|
|
518
|
+
}
|
|
519
|
+
`,
|
|
520
|
+
)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
test('replays matching idempotent requests and rejects changed input', async () => {
|
|
524
|
+
const body = { ...depositAddressBody, refundAddress: 'TKJebW6SdoJhgkKU13BeKCbrMzT6HeQhJ6' }
|
|
525
|
+
const app = transfersApp({
|
|
526
|
+
providers: [depositAddressProvider('TLrtXaP4Yp1AGqrwQLGoHD5yGh4tTAvPbe')],
|
|
527
|
+
})
|
|
528
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
529
|
+
const first = await postDepositAddress(app, { body, idempotencyKey })
|
|
530
|
+
const replay = await postDepositAddress(app, { body, idempotencyKey })
|
|
531
|
+
expect(replay.status).toBe(200)
|
|
532
|
+
expect(await replay.json()).toEqual(await first.json())
|
|
533
|
+
|
|
534
|
+
const conflict = await postDepositAddress(app, {
|
|
535
|
+
body: { ...depositAddressBody, refundAddress: 'TNR8TefgTphcrwQQodMxFDa6BPggc2CZeE' },
|
|
536
|
+
idempotencyKey,
|
|
537
|
+
})
|
|
538
|
+
expect(conflict.status).toBe(409)
|
|
539
|
+
expect(((await conflict.json()) as { error: { code: string } }).error.code).toBe(
|
|
540
|
+
'idempotency_conflict',
|
|
541
|
+
)
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
test('defaults subsidize for legacy idempotency replays', async () => {
|
|
545
|
+
const app = transfersApp({
|
|
546
|
+
providers: [depositAddressProvider(`0x${'45'.repeat(20)}`)],
|
|
547
|
+
})
|
|
548
|
+
const requestBody = {
|
|
549
|
+
...depositAddressBody,
|
|
550
|
+
refundAddress: `0x${'44'.repeat(20)}`,
|
|
551
|
+
sourceChain: 'base',
|
|
552
|
+
sourceToken: '0xfde4c96c8593536e31f229ea8f37b2ada2699bb2',
|
|
553
|
+
}
|
|
554
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
555
|
+
const first = await postDepositAddress(app, { body: requestBody, idempotencyKey })
|
|
556
|
+
const response = await TestApp.json(first, Funding.schema.createFundingDepositAddress.Response)
|
|
557
|
+
const { subsidize: _, ...legacy } = response
|
|
558
|
+
await db.kysely
|
|
559
|
+
.updateTable('funding_idempotency_requests')
|
|
560
|
+
.set({ response: JSON.stringify(legacy) })
|
|
561
|
+
.where('apiKeyId', '=', fundingWriter.id)
|
|
562
|
+
.where('keyHash', '=', Idempotency.inputHash(idempotencyKey))
|
|
563
|
+
.returningAll()
|
|
564
|
+
.executeTakeFirstOrThrow()
|
|
565
|
+
|
|
566
|
+
const replay = await postDepositAddress(app, { body: requestBody, idempotencyKey })
|
|
567
|
+
const response_replay = await TestApp.json(
|
|
568
|
+
replay,
|
|
569
|
+
Funding.schema.createFundingDepositAddress.Response,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
expect({ status: replay.status, subsidize: response_replay.subsidize }).toMatchInlineSnapshot(`
|
|
573
|
+
{
|
|
574
|
+
"status": 200,
|
|
575
|
+
"subsidize": false,
|
|
576
|
+
}
|
|
577
|
+
`)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
test('preserves the idempotency fingerprint when subsidize is omitted', async () => {
|
|
581
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
582
|
+
await FundingIdempotencyTable.claim(db, {
|
|
583
|
+
apiKeyId: fundingWriter.id,
|
|
584
|
+
keyHash: Idempotency.inputHash(idempotencyKey),
|
|
585
|
+
requestHash: Idempotency.inputHash({
|
|
586
|
+
body: depositAddressBody,
|
|
587
|
+
endpoint: 'POST /v1/funding/deposit-addresses',
|
|
588
|
+
}),
|
|
589
|
+
ttlMs: 60_000,
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
const response = await postDepositAddress(transfersApp(), { idempotencyKey })
|
|
593
|
+
|
|
594
|
+
expect({ body: await response.json(), status: response.status }).toMatchInlineSnapshot(
|
|
595
|
+
{
|
|
596
|
+
body: { requestId: expect.any(String) },
|
|
597
|
+
},
|
|
598
|
+
`
|
|
599
|
+
{
|
|
600
|
+
"body": {
|
|
601
|
+
"error": {
|
|
602
|
+
"code": "idempotency_in_progress",
|
|
603
|
+
"message": "An equivalent funding deposit address creation is still running.",
|
|
604
|
+
},
|
|
605
|
+
"requestId": Any<String>,
|
|
606
|
+
},
|
|
607
|
+
"status": 409,
|
|
608
|
+
}
|
|
609
|
+
`,
|
|
610
|
+
)
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
test('includes the creation endpoint in the idempotency fingerprint', async () => {
|
|
614
|
+
const idempotencyKey = nextDepositAddressKey()
|
|
615
|
+
await FundingIdempotencyTable.claim(db, {
|
|
616
|
+
apiKeyId: fundingWriter.id,
|
|
617
|
+
keyHash: Idempotency.inputHash(idempotencyKey),
|
|
618
|
+
requestHash: Idempotency.inputHash(depositAddressBody),
|
|
619
|
+
ttlMs: 60_000,
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
const response = await postDepositAddress(
|
|
623
|
+
transfersApp({ providers: [depositAddressProvider()] }),
|
|
624
|
+
{ idempotencyKey },
|
|
625
|
+
)
|
|
626
|
+
expect(response.status).toBe(409)
|
|
627
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
628
|
+
'idempotency_conflict',
|
|
629
|
+
)
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
test('returns not available without a registered provider', async () => {
|
|
633
|
+
const response = await postDepositAddress(transfersApp({ providers: [] }))
|
|
634
|
+
expect(response.status).toBe(404)
|
|
635
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
636
|
+
'deposit_address_not_available',
|
|
637
|
+
)
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
test('reports unexpected provider failures', async () => {
|
|
641
|
+
const entries: Log.Entry[] = []
|
|
642
|
+
const app = transfersApp({
|
|
643
|
+
logger: (entry) => {
|
|
644
|
+
entries.push(entry)
|
|
645
|
+
},
|
|
646
|
+
providers: [
|
|
647
|
+
withCreateDepositAddress('relay', async () => {
|
|
648
|
+
throw new Error('upstream failed')
|
|
649
|
+
}),
|
|
650
|
+
],
|
|
651
|
+
})
|
|
652
|
+
const response = await postDepositAddress(app, {
|
|
653
|
+
body: { ...depositAddressBody, refundAddress: 'T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb' },
|
|
654
|
+
})
|
|
655
|
+
expect(response.status).toBe(502)
|
|
656
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
657
|
+
'upstream_error',
|
|
658
|
+
)
|
|
659
|
+
expect(entries.at(-1)).toMatchObject({
|
|
660
|
+
provider: {
|
|
661
|
+
failure: 'unknown',
|
|
662
|
+
id: 'relay',
|
|
663
|
+
operation: 'createDepositAddress',
|
|
664
|
+
},
|
|
665
|
+
status: 502,
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
describe('POST /webhooks/funding/relay', () => {
|
|
671
|
+
test('authenticates and enqueues a bounded reconciliation hint', async () => {
|
|
672
|
+
const secret = 'relay-webhook-secret'
|
|
673
|
+
const address = await seedDepositAddress({
|
|
674
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
675
|
+
provider: { id: 'relay', name: 'Relay' },
|
|
676
|
+
recipient: `0x${'99'.repeat(20)}`,
|
|
677
|
+
}),
|
|
678
|
+
})
|
|
679
|
+
const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = []
|
|
680
|
+
const app = transfersApp({
|
|
681
|
+
providers: [
|
|
682
|
+
Relay.relay({
|
|
683
|
+
apiKey: secret,
|
|
684
|
+
webhook: {
|
|
685
|
+
db,
|
|
686
|
+
dispatch: async (message) => {
|
|
687
|
+
dispatched.push(message)
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
}),
|
|
691
|
+
],
|
|
692
|
+
})
|
|
693
|
+
const timestamp = String(Date.now())
|
|
694
|
+
const body = JSON.stringify({
|
|
695
|
+
data: {
|
|
696
|
+
depositAddress: { address: address.address },
|
|
697
|
+
requestId: 'relay-child-request',
|
|
698
|
+
status: 'pending',
|
|
699
|
+
updatedAt: Number(timestamp) - 1_000,
|
|
700
|
+
},
|
|
701
|
+
event: 'request.status.updated',
|
|
702
|
+
timestamp: Number(timestamp),
|
|
703
|
+
})
|
|
704
|
+
const signature = Hash.hmac256(
|
|
705
|
+
Hex.fromString(secret),
|
|
706
|
+
Hex.fromString(`${timestamp}.${body}`),
|
|
707
|
+
).slice(2)
|
|
708
|
+
const response = await app.request('/webhooks/funding/relay', {
|
|
709
|
+
body,
|
|
710
|
+
headers: {
|
|
711
|
+
'x-signature-sha256': signature,
|
|
712
|
+
'x-signature-timestamp': timestamp,
|
|
713
|
+
},
|
|
714
|
+
method: 'POST',
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
expect({ dispatched, status: response.status }).toMatchInlineSnapshot(
|
|
718
|
+
{
|
|
719
|
+
dispatched: [
|
|
720
|
+
{
|
|
721
|
+
webhookTiming: {
|
|
722
|
+
enqueuedAt: expect.any(String),
|
|
723
|
+
providerUpdatedAt: expect.any(String),
|
|
724
|
+
receivedAt: expect.any(String),
|
|
725
|
+
sentAt: expect.any(String),
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
},
|
|
730
|
+
`
|
|
731
|
+
{
|
|
732
|
+
"dispatched": [
|
|
733
|
+
{
|
|
734
|
+
"addressId": "${address.id}",
|
|
735
|
+
"requestId": "relay-child-request",
|
|
736
|
+
"trigger": "webhook",
|
|
737
|
+
"type": "funding:deposit-address:reconcile",
|
|
738
|
+
"webhookTiming": {
|
|
739
|
+
"enqueuedAt": Any<String>,
|
|
740
|
+
"providerStatus": "pending",
|
|
741
|
+
"providerUpdatedAt": Any<String>,
|
|
742
|
+
"receivedAt": Any<String>,
|
|
743
|
+
"sentAt": Any<String>,
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
],
|
|
747
|
+
"status": 204,
|
|
748
|
+
}
|
|
749
|
+
`,
|
|
750
|
+
)
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
test('rejects unauthenticated events', async () => {
|
|
754
|
+
const app = transfersApp({
|
|
755
|
+
providers: [
|
|
756
|
+
Relay.relay({
|
|
757
|
+
apiKey: 'relay-webhook-secret',
|
|
758
|
+
webhook: { db, dispatch: async () => {} },
|
|
759
|
+
}),
|
|
760
|
+
],
|
|
761
|
+
})
|
|
762
|
+
const response = await app.request('/webhooks/funding/relay', {
|
|
763
|
+
body: '{}',
|
|
764
|
+
method: 'POST',
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
expect(response.status).toMatchInlineSnapshot('401')
|
|
768
|
+
})
|
|
769
|
+
})
|
|
770
|
+
|
|
771
|
+
describe.skipIf(!relayApiKey)('POST /funding/deposit-addresses (relay)', () => {
|
|
772
|
+
test('provisions and persists a reusable address through Relay', async () => {
|
|
773
|
+
const app = transfersApp({
|
|
774
|
+
providers: [Relay.relay({ apiKey: relayApiKey })],
|
|
775
|
+
})
|
|
776
|
+
const response = await postDepositAddress(app, {
|
|
777
|
+
body: { ...depositAddressBody, recipient: `0x${'77'.repeat(20)}` },
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
expect(response.status).toBe(200)
|
|
781
|
+
const body = await TestApp.json(response, Funding.schema.createFundingDepositAddress.Response)
|
|
782
|
+
expect(body).toMatchInlineSnapshot(
|
|
783
|
+
{
|
|
784
|
+
address: expect.any(String),
|
|
785
|
+
createdAt: expect.any(String),
|
|
786
|
+
destinationAmount: {
|
|
787
|
+
baseUnits: expect.any(String),
|
|
788
|
+
formatted: expect.any(String),
|
|
789
|
+
},
|
|
790
|
+
destinationAmountMin: {
|
|
791
|
+
baseUnits: expect.any(String),
|
|
792
|
+
formatted: expect.any(String),
|
|
793
|
+
},
|
|
794
|
+
fees: expect.any(Array),
|
|
795
|
+
id: expect.any(String),
|
|
796
|
+
quote: { sampledAt: expect.any(String) },
|
|
797
|
+
updatedAt: expect.any(String),
|
|
798
|
+
},
|
|
799
|
+
`
|
|
800
|
+
{
|
|
801
|
+
"address": Any<String>,
|
|
802
|
+
"createdAt": Any<String>,
|
|
803
|
+
"destinationAmount": {
|
|
804
|
+
"baseUnits": Any<String>,
|
|
805
|
+
"currency": "USD",
|
|
806
|
+
"decimals": 6,
|
|
807
|
+
"formatted": Any<String>,
|
|
808
|
+
},
|
|
809
|
+
"destinationAmountMin": {
|
|
810
|
+
"baseUnits": Any<String>,
|
|
811
|
+
"currency": "USD",
|
|
812
|
+
"decimals": 6,
|
|
813
|
+
"formatted": Any<String>,
|
|
814
|
+
},
|
|
815
|
+
"destinationChain": {
|
|
816
|
+
"addressFormat": "hex",
|
|
817
|
+
"id": "eip155:4217",
|
|
818
|
+
"kind": "evm",
|
|
819
|
+
"name": "Tempo",
|
|
820
|
+
},
|
|
821
|
+
"destinationToken": {
|
|
822
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
823
|
+
"currency": "USD",
|
|
824
|
+
"decimals": 6,
|
|
825
|
+
"name": "USDT0",
|
|
826
|
+
"standard": "TIP-20",
|
|
827
|
+
"symbol": "USDT0",
|
|
828
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
829
|
+
"verified": true,
|
|
830
|
+
},
|
|
831
|
+
"fees": Any<Array>,
|
|
832
|
+
"id": Any<String>,
|
|
833
|
+
"provider": {
|
|
834
|
+
"id": "relay",
|
|
835
|
+
"name": "Relay",
|
|
836
|
+
},
|
|
837
|
+
"quote": {
|
|
838
|
+
"sampledAt": Any<String>,
|
|
839
|
+
},
|
|
840
|
+
"recipient": "0x7777777777777777777777777777777777777777",
|
|
841
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
842
|
+
"sourceAmount": {
|
|
843
|
+
"baseUnits": "100000000",
|
|
844
|
+
"currency": "USD",
|
|
845
|
+
"decimals": 6,
|
|
846
|
+
"formatted": "100",
|
|
847
|
+
},
|
|
848
|
+
"sourceChain": {
|
|
849
|
+
"addressFormat": "base58check",
|
|
850
|
+
"id": "tron:0x2b6653dc",
|
|
851
|
+
"kind": "tron",
|
|
852
|
+
"name": "Tron",
|
|
853
|
+
},
|
|
854
|
+
"sourceToken": {
|
|
855
|
+
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
856
|
+
"currency": "USD",
|
|
857
|
+
"decimals": 6,
|
|
858
|
+
"name": "Tether USD",
|
|
859
|
+
"standard": "TRC-20",
|
|
860
|
+
"symbol": "USDT",
|
|
861
|
+
"tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
862
|
+
"verified": true,
|
|
863
|
+
},
|
|
864
|
+
"status": "active",
|
|
865
|
+
"subsidize": false,
|
|
866
|
+
"updatedAt": Any<String>,
|
|
867
|
+
}
|
|
868
|
+
`,
|
|
869
|
+
)
|
|
870
|
+
const record = await FundingDepositAddresses.get(db, body.id)
|
|
871
|
+
expect(record).toMatchInlineSnapshot(
|
|
872
|
+
{
|
|
873
|
+
address: expect.any(String),
|
|
874
|
+
createdAt: expect.any(String),
|
|
875
|
+
id: expect.any(String),
|
|
876
|
+
nextPollAt: expect.any(String),
|
|
877
|
+
providerRequestIds: [expect.any(String)],
|
|
878
|
+
providerState: expect.any(Object),
|
|
879
|
+
snapshot: { address: expect.any(String) },
|
|
880
|
+
updatedAt: expect.any(String),
|
|
881
|
+
},
|
|
882
|
+
`
|
|
883
|
+
{
|
|
884
|
+
"address": Any<String>,
|
|
885
|
+
"apiKeyId": "key_funding_writer",
|
|
886
|
+
"createdAt": Any<String>,
|
|
887
|
+
"deliveryStrategy": "provider",
|
|
888
|
+
"destinationTokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
889
|
+
"environment": "production",
|
|
890
|
+
"id": Any<String>,
|
|
891
|
+
"lastPolledAt": null,
|
|
892
|
+
"nextPollAt": Any<String>,
|
|
893
|
+
"orgId": "org_funding",
|
|
894
|
+
"pollFailureCount": 0,
|
|
895
|
+
"pollLeaseUntil": null,
|
|
896
|
+
"pollLeaseVersion": 0,
|
|
897
|
+
"projectId": null,
|
|
898
|
+
"providerId": "relay",
|
|
899
|
+
"providerOutputToken": {
|
|
900
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
901
|
+
"currency": "USD",
|
|
902
|
+
"decimals": 6,
|
|
903
|
+
"name": "USDT0",
|
|
904
|
+
"standard": "TIP-20",
|
|
905
|
+
"symbol": "USDT0",
|
|
906
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
907
|
+
"verified": true,
|
|
908
|
+
},
|
|
909
|
+
"providerRequestIds": [
|
|
910
|
+
Any<String>,
|
|
911
|
+
],
|
|
912
|
+
"providerState": Any<Object>,
|
|
913
|
+
"recipient": "0x7777777777777777777777777777777777777777",
|
|
914
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
915
|
+
"snapshot": {
|
|
916
|
+
"address": Any<String>,
|
|
917
|
+
"destinationChain": {
|
|
918
|
+
"addressFormat": "hex",
|
|
919
|
+
"id": "eip155:4217",
|
|
920
|
+
"kind": "evm",
|
|
921
|
+
"name": "Tempo",
|
|
922
|
+
},
|
|
923
|
+
"destinationToken": {
|
|
924
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
925
|
+
"currency": "USD",
|
|
926
|
+
"decimals": 6,
|
|
927
|
+
"name": "USDT0",
|
|
928
|
+
"standard": "TIP-20",
|
|
929
|
+
"symbol": "USDT0",
|
|
930
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
931
|
+
"verified": true,
|
|
932
|
+
},
|
|
933
|
+
"provider": {
|
|
934
|
+
"id": "relay",
|
|
935
|
+
"name": "Relay",
|
|
936
|
+
},
|
|
937
|
+
"recipient": "0x7777777777777777777777777777777777777777",
|
|
938
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
939
|
+
"sourceChain": {
|
|
940
|
+
"addressFormat": "base58check",
|
|
941
|
+
"id": "tron:0x2b6653dc",
|
|
942
|
+
"kind": "tron",
|
|
943
|
+
"name": "Tron",
|
|
944
|
+
},
|
|
945
|
+
"sourceToken": {
|
|
946
|
+
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
947
|
+
"currency": "USD",
|
|
948
|
+
"decimals": 6,
|
|
949
|
+
"name": "Tether USD",
|
|
950
|
+
"standard": "TRC-20",
|
|
951
|
+
"symbol": "USDT",
|
|
952
|
+
"tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
953
|
+
"verified": true,
|
|
954
|
+
},
|
|
955
|
+
"subsidize": false,
|
|
956
|
+
},
|
|
957
|
+
"sourceChainId": "tron:0x2b6653dc",
|
|
958
|
+
"sourceTokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
959
|
+
"status": "active",
|
|
960
|
+
"subsidize": false,
|
|
961
|
+
"updatedAt": Any<String>,
|
|
962
|
+
"version": 1,
|
|
963
|
+
}
|
|
964
|
+
`,
|
|
965
|
+
)
|
|
966
|
+
})
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
describe('GET /funding/deposit-addresses/:id', () => {
|
|
970
|
+
test('requires funding:read and returns the owned address', async () => {
|
|
971
|
+
const app = transfersApp()
|
|
972
|
+
const record = await seedDepositAddress()
|
|
973
|
+
const path = `/v1/funding/deposit-addresses/${record.id}`
|
|
974
|
+
expect((await app.request(path)).status).toBe(401)
|
|
975
|
+
expect((await app.request(path, as(TestApp.key))).status).toBe(403)
|
|
976
|
+
|
|
977
|
+
const response = await app.request(path, as(fundingReader))
|
|
978
|
+
expect(response.status).toBe(200)
|
|
979
|
+
expect({
|
|
980
|
+
cacheControl: response.headers.get('cache-control'),
|
|
981
|
+
etag: response.headers.get('etag'),
|
|
982
|
+
}).toMatchInlineSnapshot(`
|
|
983
|
+
{
|
|
984
|
+
"cacheControl": "no-store",
|
|
985
|
+
"etag": null,
|
|
986
|
+
}
|
|
987
|
+
`)
|
|
988
|
+
expect(response.headers.get('vary')).toBe(
|
|
989
|
+
'Accept-Encoding, Authorization, Tempo-API-Key, X-API-Key',
|
|
990
|
+
)
|
|
991
|
+
const body = await TestApp.json(response, Funding.schema.getFundingDepositAddress.Response)
|
|
992
|
+
expect(body).toEqual(FundingDepositAddress.toPublic(record))
|
|
993
|
+
expect(body).not.toHaveProperty('providerOutputToken')
|
|
994
|
+
expect(body).not.toHaveProperty('providerRequestIds')
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
test('hides foreign, project-inaccessible, and absent addresses', async () => {
|
|
998
|
+
const app = transfersApp()
|
|
999
|
+
const record = await seedDepositAddress()
|
|
1000
|
+
const path = `/v1/funding/deposit-addresses/${record.id}`
|
|
1001
|
+
|
|
1002
|
+
for (const key of [fundingForeign, fundingProjectReader]) {
|
|
1003
|
+
const response = await app.request(path, as(key))
|
|
1004
|
+
expect(response.status).toBe(404)
|
|
1005
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1006
|
+
'funding_deposit_address_not_found',
|
|
1007
|
+
)
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const absent = await app.request(
|
|
1011
|
+
'/v1/funding/deposit-addresses/fda_000000000000000_000000000000000000000000',
|
|
1012
|
+
as(fundingReader),
|
|
1013
|
+
)
|
|
1014
|
+
expect(absent.status).toBe(404)
|
|
1015
|
+
})
|
|
1016
|
+
})
|
|
1017
|
+
|
|
1018
|
+
describe('POST /funding/deposit-addresses/:id/reconcile', () => {
|
|
1019
|
+
test('requires funding:write and queues the owned active address', async () => {
|
|
1020
|
+
const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = []
|
|
1021
|
+
const app = transfersApp({
|
|
1022
|
+
funding: {
|
|
1023
|
+
dispatchReconciliation: async (message) => {
|
|
1024
|
+
dispatched.push(message)
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
})
|
|
1028
|
+
const record = await seedDepositAddress()
|
|
1029
|
+
const path = `/v1/funding/deposit-addresses/${record.id}/reconcile`
|
|
1030
|
+
expect((await app.request(path, { method: 'POST' })).status).toBe(401)
|
|
1031
|
+
expect((await app.request(path, { ...as(fundingReader), method: 'POST' })).status).toBe(403)
|
|
1032
|
+
|
|
1033
|
+
const response = await app.request(path, { ...as(fundingWriter), method: 'POST' })
|
|
1034
|
+
expect({
|
|
1035
|
+
body: await response.text(),
|
|
1036
|
+
cacheControl: response.headers.get('cache-control'),
|
|
1037
|
+
dispatched,
|
|
1038
|
+
rateLimit: {
|
|
1039
|
+
limit: response.headers.get('ratelimit-limit'),
|
|
1040
|
+
remaining: response.headers.get('ratelimit-remaining'),
|
|
1041
|
+
scope: response.headers.get('ratelimit-scope'),
|
|
1042
|
+
},
|
|
1043
|
+
status: response.status,
|
|
1044
|
+
vary: response.headers.get('vary'),
|
|
1045
|
+
}).toMatchInlineSnapshot(`
|
|
1046
|
+
{
|
|
1047
|
+
"body": "",
|
|
1048
|
+
"cacheControl": "no-store",
|
|
1049
|
+
"dispatched": [
|
|
1050
|
+
{
|
|
1051
|
+
"addressId": "${record.id}",
|
|
1052
|
+
"trigger": "manual",
|
|
1053
|
+
"type": "funding:deposit-address:reconcile",
|
|
1054
|
+
},
|
|
1055
|
+
],
|
|
1056
|
+
"rateLimit": {
|
|
1057
|
+
"limit": "10",
|
|
1058
|
+
"remaining": "9",
|
|
1059
|
+
"scope": "funding:write",
|
|
1060
|
+
},
|
|
1061
|
+
"status": 202,
|
|
1062
|
+
"vary": "Accept-Encoding, Authorization, Tempo-API-Key",
|
|
1063
|
+
}
|
|
1064
|
+
`)
|
|
1065
|
+
expect(await FundingDepositAddresses.get(db, record.id)).toEqual(record)
|
|
1066
|
+
})
|
|
1067
|
+
|
|
1068
|
+
test('hides foreign, project-inaccessible, and absent addresses', async () => {
|
|
1069
|
+
const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = []
|
|
1070
|
+
const app = transfersApp({
|
|
1071
|
+
funding: {
|
|
1072
|
+
dispatchReconciliation: async (message) => {
|
|
1073
|
+
dispatched.push(message)
|
|
1074
|
+
},
|
|
1075
|
+
},
|
|
1076
|
+
})
|
|
1077
|
+
const record = await seedDepositAddress()
|
|
1078
|
+
const path = `/v1/funding/deposit-addresses/${record.id}/reconcile`
|
|
1079
|
+
|
|
1080
|
+
for (const key of [fundingForeignWriter, fundingProjectWriter]) {
|
|
1081
|
+
const response = await app.request(path, { ...as(key), method: 'POST' })
|
|
1082
|
+
expect(response.status).toBe(404)
|
|
1083
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1084
|
+
'funding_deposit_address_not_found',
|
|
1085
|
+
)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const absent = await app.request(
|
|
1089
|
+
'/v1/funding/deposit-addresses/fda_000000000000000_000000000000000000000000/reconcile',
|
|
1090
|
+
{ ...as(fundingWriter), method: 'POST' },
|
|
1091
|
+
)
|
|
1092
|
+
expect(absent.status).toBe(404)
|
|
1093
|
+
expect(dispatched).toEqual([])
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
test('rejects inactive addresses and unavailable reconciliation', async () => {
|
|
1097
|
+
const dispatched: FundingProvider.Webhook.receive.Dispatchable[] = []
|
|
1098
|
+
const configured = transfersApp({
|
|
1099
|
+
funding: {
|
|
1100
|
+
dispatchReconciliation: async (message) => {
|
|
1101
|
+
dispatched.push(message)
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
})
|
|
1105
|
+
const record = await seedDepositAddress()
|
|
1106
|
+
const transition = await FundingDepositAddress.transition(db, {
|
|
1107
|
+
expectedVersion: record.version,
|
|
1108
|
+
id: record.id,
|
|
1109
|
+
status: 'action-required',
|
|
1110
|
+
})
|
|
1111
|
+
expect(transition.type).toBe('applied')
|
|
1112
|
+
|
|
1113
|
+
const inactive = await configured.request(
|
|
1114
|
+
`/v1/funding/deposit-addresses/${record.id}/reconcile`,
|
|
1115
|
+
{ ...as(fundingWriter), method: 'POST' },
|
|
1116
|
+
)
|
|
1117
|
+
expect({
|
|
1118
|
+
code: ((await inactive.json()) as { error: { code: string } }).error.code,
|
|
1119
|
+
status: inactive.status,
|
|
1120
|
+
}).toEqual({ code: 'funding_deposit_address_not_active', status: 409 })
|
|
1121
|
+
|
|
1122
|
+
const active = await seedDepositAddress()
|
|
1123
|
+
const unavailable = await transfersApp().request(
|
|
1124
|
+
`/v1/funding/deposit-addresses/${active.id}/reconcile`,
|
|
1125
|
+
{ ...as(fundingWriter), method: 'POST' },
|
|
1126
|
+
)
|
|
1127
|
+
expect({
|
|
1128
|
+
code: ((await unavailable.json()) as { error: { code: string } }).error.code,
|
|
1129
|
+
status: unavailable.status,
|
|
1130
|
+
}).toEqual({ code: 'not_implemented', status: 501 })
|
|
1131
|
+
expect(dispatched).toEqual([])
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
test('returns an upstream error when the queue rejects the message', async () => {
|
|
1135
|
+
const app = transfersApp({
|
|
1136
|
+
funding: {
|
|
1137
|
+
dispatchReconciliation: async () => {
|
|
1138
|
+
throw new Error('queue unavailable')
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
})
|
|
1142
|
+
const record = await seedDepositAddress()
|
|
1143
|
+
const response = await app.request(`/v1/funding/deposit-addresses/${record.id}/reconcile`, {
|
|
1144
|
+
...as(fundingWriter),
|
|
1145
|
+
method: 'POST',
|
|
1146
|
+
})
|
|
1147
|
+
|
|
1148
|
+
expect({
|
|
1149
|
+
code: ((await response.json()) as { error: { code: string } }).error.code,
|
|
1150
|
+
status: response.status,
|
|
1151
|
+
}).toEqual({ code: 'upstream_error', status: 502 })
|
|
1152
|
+
})
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
describe('GET /funding/deposit-addresses/:id/deposits', () => {
|
|
1156
|
+
test('pages deposits newest-first under an owned address', async () => {
|
|
1157
|
+
const app = transfersApp()
|
|
1158
|
+
const address = await seedDepositAddress()
|
|
1159
|
+
const first = await seedDeposit(address)
|
|
1160
|
+
const second = await seedDeposit(address)
|
|
1161
|
+
const third = await seedDeposit(address)
|
|
1162
|
+
const fourth = await seedDeposit(address)
|
|
1163
|
+
const fifth = await seedDeposit(address)
|
|
1164
|
+
const sixth = await seedDeposit(address)
|
|
1165
|
+
|
|
1166
|
+
const response = await app.request(
|
|
1167
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits?limit=5`,
|
|
1168
|
+
as(fundingReader),
|
|
1169
|
+
)
|
|
1170
|
+
expect(response.status).toBe(200)
|
|
1171
|
+
expect({
|
|
1172
|
+
cacheControl: response.headers.get('cache-control'),
|
|
1173
|
+
etag: response.headers.get('etag'),
|
|
1174
|
+
}).toMatchInlineSnapshot(`
|
|
1175
|
+
{
|
|
1176
|
+
"cacheControl": "no-store",
|
|
1177
|
+
"etag": null,
|
|
1178
|
+
}
|
|
1179
|
+
`)
|
|
1180
|
+
const body = await TestApp.json(
|
|
1181
|
+
response,
|
|
1182
|
+
Funding.schema.listFundingDepositAddressDeposits.Response,
|
|
1183
|
+
)
|
|
1184
|
+
expect(body.data.map((deposit) => deposit.id)).toEqual([
|
|
1185
|
+
sixth.id,
|
|
1186
|
+
fifth.id,
|
|
1187
|
+
fourth.id,
|
|
1188
|
+
third.id,
|
|
1189
|
+
second.id,
|
|
1190
|
+
])
|
|
1191
|
+
expect(body.nextCursor).toBe(Cursor.encode([second.createdAt, second.id]))
|
|
1192
|
+
|
|
1193
|
+
const next = await TestApp.json(
|
|
1194
|
+
await app.request(
|
|
1195
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits?limit=5&cursor=${body.nextCursor}`,
|
|
1196
|
+
as(fundingReader),
|
|
1197
|
+
),
|
|
1198
|
+
Funding.schema.listFundingDepositAddressDeposits.Response,
|
|
1199
|
+
)
|
|
1200
|
+
expect(next.data.map((deposit) => deposit.id)).toEqual([first.id])
|
|
1201
|
+
expect(next.nextCursor).toBeNull()
|
|
1202
|
+
})
|
|
1203
|
+
|
|
1204
|
+
test('returns a provider-observed deposit before source verification', async () => {
|
|
1205
|
+
const app = transfersApp()
|
|
1206
|
+
const address = await seedDepositAddress({
|
|
1207
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1208
|
+
provider: { id: 'relay_observed', name: 'Relay' },
|
|
1209
|
+
recipient: `0x${'31'.repeat(20)}`,
|
|
1210
|
+
}),
|
|
1211
|
+
})
|
|
1212
|
+
const sourceTransactionHash = `0x${'ab'.repeat(32)}`
|
|
1213
|
+
await FundingDeposit.create(db, {
|
|
1214
|
+
now: new Date('2026-01-04T00:00:00.000Z'),
|
|
1215
|
+
providerRequestId: 'relay-observed-request',
|
|
1216
|
+
snapshot: {
|
|
1217
|
+
depositAddressId: address.id,
|
|
1218
|
+
destinationChain: address.snapshot.destinationChain,
|
|
1219
|
+
destinationToken: address.snapshot.destinationToken,
|
|
1220
|
+
provider: address.snapshot.provider,
|
|
1221
|
+
recipient: address.recipient,
|
|
1222
|
+
refundAddress: address.refundAddress,
|
|
1223
|
+
sourceChain: address.snapshot.sourceChain,
|
|
1224
|
+
sourceToken: address.snapshot.sourceToken,
|
|
1225
|
+
sourceTransactionHashes: [sourceTransactionHash],
|
|
1226
|
+
},
|
|
1227
|
+
sourceTransactionHash,
|
|
1228
|
+
})
|
|
1229
|
+
|
|
1230
|
+
const response = await app.request(
|
|
1231
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits`,
|
|
1232
|
+
as(fundingReader),
|
|
1233
|
+
)
|
|
1234
|
+
expect(response.status).toBe(200)
|
|
1235
|
+
const body = await TestApp.json(
|
|
1236
|
+
response,
|
|
1237
|
+
Funding.schema.listFundingDepositAddressDeposits.Response,
|
|
1238
|
+
)
|
|
1239
|
+
expect(body).toMatchInlineSnapshot(
|
|
1240
|
+
{
|
|
1241
|
+
data: [
|
|
1242
|
+
{
|
|
1243
|
+
createdAt: expect.any(String),
|
|
1244
|
+
depositAddressId: expect.any(String),
|
|
1245
|
+
id: expect.any(String),
|
|
1246
|
+
updatedAt: expect.any(String),
|
|
1247
|
+
},
|
|
1248
|
+
],
|
|
1249
|
+
},
|
|
1250
|
+
`
|
|
1251
|
+
{
|
|
1252
|
+
"data": [
|
|
1253
|
+
{
|
|
1254
|
+
"createdAt": Any<String>,
|
|
1255
|
+
"depositAddressId": Any<String>,
|
|
1256
|
+
"destinationChain": {
|
|
1257
|
+
"addressFormat": "hex",
|
|
1258
|
+
"id": "eip155:4217",
|
|
1259
|
+
"kind": "evm",
|
|
1260
|
+
"name": "Tempo",
|
|
1261
|
+
},
|
|
1262
|
+
"destinationToken": {
|
|
1263
|
+
"address": "0x20c00000000000000000000014f22ca97301eb73",
|
|
1264
|
+
"currency": "USD",
|
|
1265
|
+
"decimals": 6,
|
|
1266
|
+
"name": "USDT0",
|
|
1267
|
+
"standard": "TIP-20",
|
|
1268
|
+
"symbol": "USDT0",
|
|
1269
|
+
"tokenKey": "eip155:4217/erc20:0x20c00000000000000000000014f22ca97301eb73",
|
|
1270
|
+
"verified": true,
|
|
1271
|
+
},
|
|
1272
|
+
"id": Any<String>,
|
|
1273
|
+
"provider": {
|
|
1274
|
+
"id": "relay_observed",
|
|
1275
|
+
"name": "Relay",
|
|
1276
|
+
},
|
|
1277
|
+
"recipient": "0x3131313131313131313131313131313131313131",
|
|
1278
|
+
"refundAddress": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
|
|
1279
|
+
"sourceChain": {
|
|
1280
|
+
"addressFormat": "base58check",
|
|
1281
|
+
"id": "tron:0x2b6653dc",
|
|
1282
|
+
"kind": "tron",
|
|
1283
|
+
"name": "Tron",
|
|
1284
|
+
},
|
|
1285
|
+
"sourceToken": {
|
|
1286
|
+
"address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1287
|
+
"currency": "USD",
|
|
1288
|
+
"decimals": 6,
|
|
1289
|
+
"name": "Tether USD",
|
|
1290
|
+
"standard": "TRC-20",
|
|
1291
|
+
"symbol": "USDT",
|
|
1292
|
+
"tokenKey": "tron:0x2b6653dc/trc20:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
|
|
1293
|
+
"verified": true,
|
|
1294
|
+
},
|
|
1295
|
+
"sourceTransactionHashes": [
|
|
1296
|
+
"0xabababababababababababababababababababababababababababababababab",
|
|
1297
|
+
],
|
|
1298
|
+
"status": "detected",
|
|
1299
|
+
"updatedAt": Any<String>,
|
|
1300
|
+
},
|
|
1301
|
+
],
|
|
1302
|
+
"nextCursor": null,
|
|
1303
|
+
}
|
|
1304
|
+
`,
|
|
1305
|
+
)
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
test('returns the parent not-found error before listing deposits', async () => {
|
|
1309
|
+
const app = transfersApp()
|
|
1310
|
+
const address = await seedDepositAddress({ orgId: fundingForeign.orgId })
|
|
1311
|
+
const response = await app.request(
|
|
1312
|
+
`/v1/funding/deposit-addresses/${address.id}/deposits`,
|
|
1313
|
+
as(fundingReader),
|
|
1314
|
+
)
|
|
1315
|
+
expect(response.status).toBe(404)
|
|
1316
|
+
expect(((await response.json()) as { error: { code: string } }).error.code).toBe(
|
|
1317
|
+
'funding_deposit_address_not_found',
|
|
1318
|
+
)
|
|
1319
|
+
})
|
|
1320
|
+
})
|
|
1321
|
+
|
|
1322
|
+
describe('GET /funding/deposits', () => {
|
|
1323
|
+
test('requires funding:read and pages recipient deposits across addresses', async () => {
|
|
1324
|
+
const app = transfersApp()
|
|
1325
|
+
const recipient = '0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD'
|
|
1326
|
+
const path = `/v1/funding/deposits?recipient=${recipient.toLowerCase()}`
|
|
1327
|
+
expect((await app.request(path)).status).toBe(401)
|
|
1328
|
+
expect((await app.request(path, as(TestApp.key))).status).toBe(403)
|
|
1329
|
+
expect((await app.request('/v1/funding/deposits', as(fundingReader))).status).toBe(400)
|
|
1330
|
+
|
|
1331
|
+
const firstAddress = await seedDepositAddress({
|
|
1332
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1333
|
+
provider: { id: 'relay_account_first', name: 'Relay' },
|
|
1334
|
+
recipient,
|
|
1335
|
+
}),
|
|
1336
|
+
})
|
|
1337
|
+
const secondAddress = await seedDepositAddress({
|
|
1338
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1339
|
+
provider: { id: 'relay_account_second', name: 'Relay' },
|
|
1340
|
+
recipient,
|
|
1341
|
+
subsidize: true,
|
|
1342
|
+
}),
|
|
1343
|
+
})
|
|
1344
|
+
const first = await seedDeposit(firstAddress)
|
|
1345
|
+
const second = await seedDeposit(secondAddress)
|
|
1346
|
+
const third = await seedDeposit(firstAddress)
|
|
1347
|
+
const fourth = await seedDeposit(secondAddress)
|
|
1348
|
+
const fifth = await seedDeposit(firstAddress)
|
|
1349
|
+
const sixth = await seedDeposit(secondAddress)
|
|
1350
|
+
const unrelatedAddress = await seedDepositAddress()
|
|
1351
|
+
await seedDeposit(unrelatedAddress)
|
|
1352
|
+
const foreignAddress = await seedDepositAddress({
|
|
1353
|
+
orgId: fundingForeign.orgId,
|
|
1354
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1355
|
+
provider: { id: 'relay_account_foreign', name: 'Relay' },
|
|
1356
|
+
recipient,
|
|
1357
|
+
}),
|
|
1358
|
+
})
|
|
1359
|
+
await seedDeposit(foreignAddress)
|
|
1360
|
+
|
|
1361
|
+
const response = await app.request(`${path}&limit=5`, as(fundingReader))
|
|
1362
|
+
expect(response.status).toBe(200)
|
|
1363
|
+
expect({
|
|
1364
|
+
cacheControl: response.headers.get('cache-control'),
|
|
1365
|
+
etag: response.headers.get('etag'),
|
|
1366
|
+
vary: response.headers.get('vary'),
|
|
1367
|
+
}).toMatchInlineSnapshot(`
|
|
1368
|
+
{
|
|
1369
|
+
"cacheControl": "no-store",
|
|
1370
|
+
"etag": null,
|
|
1371
|
+
"vary": "Accept-Encoding, Authorization, Tempo-API-Key",
|
|
1372
|
+
}
|
|
1373
|
+
`)
|
|
1374
|
+
const body = await TestApp.json(response, Funding.schema.listFundingDeposits.Response)
|
|
1375
|
+
expect(body.data.map((deposit) => deposit.id)).toEqual([
|
|
1376
|
+
sixth.id,
|
|
1377
|
+
fifth.id,
|
|
1378
|
+
fourth.id,
|
|
1379
|
+
third.id,
|
|
1380
|
+
second.id,
|
|
1381
|
+
])
|
|
1382
|
+
expect(body.nextCursor).toBe(Cursor.encode([second.createdAt, second.id]))
|
|
1383
|
+
|
|
1384
|
+
const next = await TestApp.json(
|
|
1385
|
+
await app.request(`${path}&limit=5&cursor=${body.nextCursor}`, as(fundingReader)),
|
|
1386
|
+
Funding.schema.listFundingDeposits.Response,
|
|
1387
|
+
)
|
|
1388
|
+
expect(next.data.map((deposit) => deposit.id)).toEqual([first.id])
|
|
1389
|
+
expect(next.nextCursor).toBeNull()
|
|
1390
|
+
})
|
|
1391
|
+
|
|
1392
|
+
test('narrows project-attributed keys to their project', async () => {
|
|
1393
|
+
const app = transfersApp()
|
|
1394
|
+
const recipient = `0x${'ab'.repeat(20)}`
|
|
1395
|
+
const projectAddress = await seedDepositAddress({
|
|
1396
|
+
projectId: fundingProjectReader.projectId,
|
|
1397
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1398
|
+
provider: { id: 'relay_account_project', name: 'Relay' },
|
|
1399
|
+
recipient,
|
|
1400
|
+
}),
|
|
1401
|
+
})
|
|
1402
|
+
const siblingAddress = await seedDepositAddress({
|
|
1403
|
+
projectId: 'prj_funding_other',
|
|
1404
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1405
|
+
provider: { id: 'relay_account_sibling', name: 'Relay' },
|
|
1406
|
+
recipient,
|
|
1407
|
+
}),
|
|
1408
|
+
})
|
|
1409
|
+
const projectDeposit = await seedDeposit(projectAddress)
|
|
1410
|
+
await seedDeposit(siblingAddress)
|
|
1411
|
+
|
|
1412
|
+
const body = await TestApp.json(
|
|
1413
|
+
await app.request(`/v1/funding/deposits?recipient=${recipient}`, as(fundingProjectReader)),
|
|
1414
|
+
Funding.schema.listFundingDeposits.Response,
|
|
1415
|
+
)
|
|
1416
|
+
expect(body.data.map((deposit) => deposit.id)).toEqual([projectDeposit.id])
|
|
1417
|
+
})
|
|
1418
|
+
})
|
|
1419
|
+
|
|
1420
|
+
describe('GET /funding/deposits/:id', () => {
|
|
1421
|
+
test('returns the owned deposit without private reconciliation state', async () => {
|
|
1422
|
+
const app = transfersApp()
|
|
1423
|
+
const address = await seedDepositAddress()
|
|
1424
|
+
const record = await seedDeposit(address, {
|
|
1425
|
+
providerRequestIds: ['relay-private'],
|
|
1426
|
+
providerState: { request: 'private' },
|
|
1427
|
+
})
|
|
1428
|
+
const response = await app.request(`/v1/funding/deposits/${record.id}`, as(fundingReader))
|
|
1429
|
+
expect(response.status).toBe(200)
|
|
1430
|
+
expect(response.headers.get('cache-control')).toBe(Cache.policies.state)
|
|
1431
|
+
const body = await TestApp.json(response, Funding.schema.getFundingDeposit.Response)
|
|
1432
|
+
expect(body).toEqual(FundingDeposit.toPublic(record))
|
|
1433
|
+
expect(body).not.toHaveProperty('providerRequestIds')
|
|
1434
|
+
expect(body).not.toHaveProperty('providerState')
|
|
1435
|
+
})
|
|
1436
|
+
|
|
1437
|
+
test('hides foreign and absent deposits', async () => {
|
|
1438
|
+
const app = transfersApp()
|
|
1439
|
+
const address = await seedDepositAddress()
|
|
1440
|
+
const record = await seedDeposit(address)
|
|
1441
|
+
expect(
|
|
1442
|
+
(await app.request(`/v1/funding/deposits/${record.id}`, as(fundingForeign))).status,
|
|
1443
|
+
).toBe(404)
|
|
1444
|
+
expect(
|
|
1445
|
+
(
|
|
1446
|
+
await app.request(
|
|
1447
|
+
'/v1/funding/deposits/fdp_000000000000000_000000000000000000000000',
|
|
1448
|
+
as(fundingReader),
|
|
1449
|
+
)
|
|
1450
|
+
).status,
|
|
1451
|
+
).toBe(404)
|
|
1452
|
+
})
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
const depositAddressBody = {
|
|
1456
|
+
amount: '100000000',
|
|
1457
|
+
destinationToken: '0x20c00000000000000000000014f22ca97301eb73',
|
|
1458
|
+
recipient: `0x${'11'.repeat(20)}`,
|
|
1459
|
+
refundAddress: 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8',
|
|
1460
|
+
sourceChain: 'tron',
|
|
1461
|
+
sourceToken: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t',
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
let depositAddressIdempotencySequence = 0
|
|
1465
|
+
|
|
1466
|
+
function nextDepositAddressKey() {
|
|
1467
|
+
return `funding_deposit_address_${depositAddressIdempotencySequence++}`
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
function postDepositAddress(
|
|
1471
|
+
app: ReturnType<typeof create>,
|
|
1472
|
+
options: {
|
|
1473
|
+
body?: unknown
|
|
1474
|
+
headers?: Record<string, string> | undefined
|
|
1475
|
+
idempotencyKey?: string | undefined
|
|
1476
|
+
key?: { token: string } | undefined
|
|
1477
|
+
} = {},
|
|
1478
|
+
) {
|
|
1479
|
+
return app.request('/v1/funding/deposit-addresses', {
|
|
1480
|
+
body: JSON.stringify(options.body ?? depositAddressBody),
|
|
1481
|
+
headers: options.headers ?? {
|
|
1482
|
+
authorization: `Bearer ${(options.key ?? fundingWriter).token}`,
|
|
1483
|
+
'content-type': 'application/json',
|
|
1484
|
+
'idempotency-key': options.idempotencyKey ?? nextDepositAddressKey(),
|
|
1485
|
+
},
|
|
1486
|
+
method: 'POST',
|
|
1487
|
+
})
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
function withCreateDepositAddress(
|
|
1491
|
+
id: FundingProvider.ProviderId,
|
|
1492
|
+
createDepositAddress: NonNullable<FundingProvider.Provider['createDepositAddress']>,
|
|
1493
|
+
) {
|
|
1494
|
+
const provider = providers.find((entry) => entry.id === id)
|
|
1495
|
+
if (!provider) throw new Error(`Missing ${id} provider`)
|
|
1496
|
+
return FundingProvider.from({ ...provider, createDepositAddress })
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
function depositAddressProvider(
|
|
1500
|
+
address = 'TJRabPrwbZy45sbavfcjinPJC18kjpRTv8',
|
|
1501
|
+
onCreate: (input: FundingProvider.createDepositAddress.Parameters) => void = () => {},
|
|
1502
|
+
) {
|
|
1503
|
+
return withCreateDepositAddress('relay', async (input) => {
|
|
1504
|
+
onCreate(input)
|
|
1505
|
+
return {
|
|
1506
|
+
address,
|
|
1507
|
+
correlation: {
|
|
1508
|
+
checkEndpoint: '/intents/status/v3?requestId=relay_request_1',
|
|
1509
|
+
requestId: 'relay_request_1',
|
|
1510
|
+
},
|
|
1511
|
+
destinationAmount: '1000000',
|
|
1512
|
+
destinationAmountMin: '1000000',
|
|
1513
|
+
fees: [],
|
|
1514
|
+
sampledAt: '2026-08-04T00:00:00.000Z',
|
|
1515
|
+
}
|
|
1516
|
+
})
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const fundingReader = {
|
|
1520
|
+
id: 'key_funding',
|
|
1521
|
+
orgId: 'org_funding',
|
|
1522
|
+
scopes: ['funding:read'],
|
|
1523
|
+
token: 'secret_funding_reader',
|
|
1524
|
+
} satisfies TestApp.kvStore.Key
|
|
1525
|
+
|
|
1526
|
+
const fundingProjectReader = {
|
|
1527
|
+
id: 'key_funding_project',
|
|
1528
|
+
orgId: 'org_funding',
|
|
1529
|
+
projectId: 'prj_funding',
|
|
1530
|
+
scopes: ['funding:read'],
|
|
1531
|
+
token: 'secret_funding_project',
|
|
1532
|
+
} satisfies TestApp.kvStore.Key
|
|
1533
|
+
|
|
1534
|
+
const fundingForeign = {
|
|
1535
|
+
id: 'key_funding_other',
|
|
1536
|
+
orgId: 'org_funding_other',
|
|
1537
|
+
scopes: ['funding:read'],
|
|
1538
|
+
token: 'secret_funding_other',
|
|
1539
|
+
} satisfies TestApp.kvStore.Key
|
|
1540
|
+
|
|
1541
|
+
/** Key with funding write access in a different organization. */
|
|
1542
|
+
const fundingForeignWriter = {
|
|
1543
|
+
id: 'key_funding_other_writer',
|
|
1544
|
+
orgId: fundingForeign.orgId,
|
|
1545
|
+
scopes: ['funding:write'],
|
|
1546
|
+
token: 'secret_funding_other_writer',
|
|
1547
|
+
} satisfies TestApp.kvStore.Key
|
|
1548
|
+
|
|
1549
|
+
/** Project-attributed funding writer in the owning organization. */
|
|
1550
|
+
const fundingProjectWriter = {
|
|
1551
|
+
id: 'key_funding_project_writer',
|
|
1552
|
+
orgId: fundingReader.orgId,
|
|
1553
|
+
projectId: fundingProjectReader.projectId,
|
|
1554
|
+
scopes: ['funding:write'],
|
|
1555
|
+
token: 'secret_funding_project_writer',
|
|
1556
|
+
} satisfies TestApp.kvStore.Key
|
|
1557
|
+
|
|
1558
|
+
const fundingWriter = {
|
|
1559
|
+
id: 'key_funding_writer',
|
|
1560
|
+
orgId: 'org_funding',
|
|
1561
|
+
scopes: ['funding:read', 'funding:write'],
|
|
1562
|
+
token: 'secret_funding_writer',
|
|
1563
|
+
} satisfies TestApp.kvStore.Key
|
|
1564
|
+
|
|
1565
|
+
function transfersApp(options: TestApp.create.Options = {}) {
|
|
1566
|
+
return create({
|
|
1567
|
+
...options,
|
|
1568
|
+
auth: {
|
|
1569
|
+
keys: [
|
|
1570
|
+
TestApp.key,
|
|
1571
|
+
fundingForeign,
|
|
1572
|
+
fundingForeignWriter,
|
|
1573
|
+
fundingProjectReader,
|
|
1574
|
+
fundingProjectWriter,
|
|
1575
|
+
fundingReader,
|
|
1576
|
+
fundingWriter,
|
|
1577
|
+
],
|
|
1578
|
+
},
|
|
1579
|
+
defaultChainId: options.defaultChainId ?? tempoMainnet.id,
|
|
1580
|
+
})
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
function as(key: { token: string }) {
|
|
1584
|
+
return { headers: { authorization: `Bearer ${key.token}` } } as const
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
let seedDepositAddressSecond = 0
|
|
1588
|
+
|
|
1589
|
+
function seedDepositAddress(input: Partial<FundingDepositAddress.create.Input> = {}) {
|
|
1590
|
+
const index = seedDepositAddressSecond++
|
|
1591
|
+
const now = new Date(Date.UTC(2026, 0, 3, 0, 0, index))
|
|
1592
|
+
return FundingDepositAddress.create(db, {
|
|
1593
|
+
apiKeyId: fundingReader.id,
|
|
1594
|
+
deliveryStrategy: 'provider',
|
|
1595
|
+
environment: 'production',
|
|
1596
|
+
id: FundingDepositAddress.generateId(now),
|
|
1597
|
+
now,
|
|
1598
|
+
orgId: fundingReader.orgId,
|
|
1599
|
+
providerOutputToken: TestFunding.providerOutputToken(),
|
|
1600
|
+
snapshot: TestFunding.depositAddressSnapshot({
|
|
1601
|
+
provider: { id: `relay_route_${index}`, name: 'Relay' },
|
|
1602
|
+
recipient: `0x${index.toString(16).padStart(40, '0')}`,
|
|
1603
|
+
}),
|
|
1604
|
+
...input,
|
|
1605
|
+
})
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
let seedDepositSecond = 0
|
|
1609
|
+
|
|
1610
|
+
function seedDeposit(
|
|
1611
|
+
address: Awaited<ReturnType<typeof seedDepositAddress>>,
|
|
1612
|
+
input: Partial<FundingDeposit.create.Input> = {},
|
|
1613
|
+
) {
|
|
1614
|
+
const index = seedDepositSecond++
|
|
1615
|
+
const now = new Date(Date.UTC(2026, 0, 4, 0, 0, index))
|
|
1616
|
+
const sourceTransactionHash = index.toString(16).padStart(64, '0')
|
|
1617
|
+
return FundingDeposit.create(db, {
|
|
1618
|
+
now,
|
|
1619
|
+
providerRequestId: `relay_request_${index}`,
|
|
1620
|
+
snapshot: TestFunding.depositSnapshot(address.id, {
|
|
1621
|
+
provider: address.snapshot.provider,
|
|
1622
|
+
sourceTransactionHashes: [sourceTransactionHash],
|
|
1623
|
+
}),
|
|
1624
|
+
sourceTransactionHash,
|
|
1625
|
+
sourceTransferIndex: 0,
|
|
1626
|
+
...input,
|
|
1627
|
+
})
|
|
1628
|
+
}
|