tapimo 0.12.0 → 0.13.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/dist/App.d.ts +5561 -5026
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +38 -3
- package/dist/App.js.map +1 -1
- package/dist/Scope.d.ts +2 -0
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +17 -0
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +123 -74
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +80 -0
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +54 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/earn-vaults.d.ts +15 -15
- package/dist/admin/apps/earn-vaults.js +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +81 -81
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +151 -151
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/apps/Zones.d.ts +16 -16
- package/dist/apps/Zones.js +4 -4
- package/dist/apps/data/App.js +18 -18
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/ZoneSelection.d.ts +8 -8
- package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
- package/dist/apps/data/ZoneSelection.js +1 -1
- package/dist/apps/data/ZoneSelection.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +588 -584
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +1561 -1239
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +26 -26
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +43 -16
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +44 -44
- package/dist/apps/data/routes/blocks.d.ts.map +1 -1
- package/dist/apps/data/routes/blocks.js +21 -11
- package/dist/apps/data/routes/blocks.js.map +1 -1
- package/dist/apps/data/routes/coingecko.d.ts +3 -3
- package/dist/apps/data/routes/earn.d.ts +18 -18
- package/dist/apps/data/routes/earn.js +13 -13
- package/dist/apps/data/routes/exchanges.d.ts +145 -145
- package/dist/apps/data/routes/fee-amm.d.ts +2 -2
- package/dist/apps/data/routes/receipts.d.ts +155 -55
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +28 -10
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +13 -13
- package/dist/apps/data/routes/tokens.d.ts +98 -98
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/tokens.js +51 -20
- package/dist/apps/data/routes/tokens.js.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +7 -2
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +37 -17
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +1 -1
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +18 -12
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +1 -1
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +12 -7
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
- package/dist/apps/data/routes/webhooks.d.ts +11 -11
- package/dist/apps/data/routes/zones.d.ts +14 -14
- package/dist/apps/data/routes/zones.js +1 -1
- package/dist/apps/funding/App.d.ts +11 -0
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +16 -11
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +60 -48
- package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
- package/dist/apps/funding/routes/transfers.js +27 -19
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +4487 -3963
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +710 -159
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +236 -4
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +6 -6
- package/dist/apps/management/routes/invite-links.d.ts +42 -42
- package/dist/apps/management/routes/members.d.ts +30 -30
- package/dist/apps/management/routes/projects.d.ts +40 -40
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/management/routes/verified-token-requests.d.ts +1 -1
- package/dist/apps/management/routes/webhooks.d.ts +55 -55
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +12 -1
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +3 -2
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +5 -2
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/fundingCatalog.d.ts +4 -2
- package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
- package/dist/db/tables/fundingCatalog.js +5 -1
- package/dist/db/tables/fundingCatalog.js.map +1 -1
- package/dist/handlers/internal/utils.d.ts.map +1 -1
- package/dist/handlers/internal/utils.js +9 -0
- package/dist/handlers/internal/utils.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -1
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -3
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/EarnVaults.d.ts +3 -3
- package/dist/internal/EarnVaults.js +3 -3
- package/dist/internal/Fetch.d.ts +23 -0
- package/dist/internal/Fetch.d.ts.map +1 -0
- package/dist/internal/Fetch.js +26 -0
- package/dist/internal/Fetch.js.map +1 -0
- package/dist/internal/Log.d.ts +3 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +16 -3
- package/dist/internal/Log.js.map +1 -1
- package/dist/internal/OpenApi.d.ts +5 -3
- package/dist/internal/OpenApi.d.ts.map +1 -1
- package/dist/internal/OpenApi.js +13 -3
- package/dist/internal/OpenApi.js.map +1 -1
- package/dist/internal/Response.d.ts +1 -1
- package/dist/internal/Response.d.ts.map +1 -1
- package/dist/internal/Schema.d.ts +6 -3
- package/dist/internal/Schema.d.ts.map +1 -1
- package/dist/internal/Schema.js +8 -2
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +16 -3
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +82 -16
- package/dist/internal/Store.js.map +1 -1
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +7 -2
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Timeout.d.ts +36 -0
- package/dist/internal/Timeout.d.ts.map +1 -0
- package/dist/internal/Timeout.js +37 -0
- package/dist/internal/Timeout.js.map +1 -0
- package/dist/internal/Timing.d.ts +9 -0
- package/dist/internal/Timing.d.ts.map +1 -1
- package/dist/internal/Timing.js +7 -0
- package/dist/internal/Timing.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +3 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/funding/Action.d.ts +9 -0
- package/dist/internal/funding/Action.d.ts.map +1 -1
- package/dist/internal/funding/Action.js +16 -8
- package/dist/internal/funding/Action.js.map +1 -1
- package/dist/internal/funding/Catalog.d.ts +9 -2
- package/dist/internal/funding/Catalog.d.ts.map +1 -1
- package/dist/internal/funding/Catalog.js +19 -8
- package/dist/internal/funding/Catalog.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +68 -13
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +29 -8
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Transfer.d.ts +13 -12
- package/dist/internal/funding/Transfer.d.ts.map +1 -1
- package/dist/internal/funding/Transfer.js +23 -21
- package/dist/internal/funding/Transfer.js.map +1 -1
- package/dist/internal/funding/providers/stargate.d.ts +28 -9
- package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
- package/dist/internal/funding/providers/stargate.js +251 -117
- package/dist/internal/funding/providers/stargate.js.map +1 -1
- package/dist/internal/index.d.ts +1 -0
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +1 -0
- package/dist/internal/index.js.map +1 -1
- package/package.json +2 -2
- package/src/App.test.ts +100 -0
- package/src/App.ts +42 -3
- package/src/Client.test-d.ts +46 -0
- package/src/Scope.test.ts +20 -0
- package/src/Scope.ts +16 -0
- package/src/admin/apps/api-keys.test.ts +75 -0
- package/src/admin/apps/api-keys.ts +64 -0
- package/src/admin/apps/earn-vaults.test.ts +1 -1
- package/src/admin/apps/earn-vaults.ts +1 -1
- package/src/admin/ui/src/lib/api-keys.ts +19 -0
- package/src/admin/ui/src/routes/api-keys.tsx +89 -2
- package/src/admin/ui.generated.ts +189 -151
- package/src/apps/Zones.test.ts +4 -4
- package/src/apps/Zones.ts +4 -4
- package/src/apps/composition.test.ts +1 -1
- package/src/apps/data/App.test.ts +41 -0
- package/src/apps/data/App.ts +18 -18
- package/src/apps/data/ZoneSelection.test.ts +2 -2
- package/src/apps/data/ZoneSelection.ts +8 -8
- package/src/apps/data/routes/activities.test.ts +1526 -12
- package/src/apps/data/routes/activities.ts +1774 -1356
- package/src/apps/data/routes/balances.ts +133 -97
- package/src/apps/data/routes/blocks.test.ts +533 -0
- package/src/apps/data/routes/blocks.ts +35 -16
- package/src/apps/data/routes/earn.test.ts +4 -2
- package/src/apps/data/routes/earn.ts +13 -13
- package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
- package/src/apps/data/routes/receipts.ts +47 -16
- package/src/apps/data/routes/tokens.test.ts +578 -0
- package/src/apps/data/routes/tokens.ts +283 -225
- package/src/apps/data/routes/transactions.ts +68 -29
- package/src/apps/data/routes/transfers.test.ts +421 -0
- package/src/apps/data/routes/transfers.ts +45 -27
- package/src/apps/data/routes/valuation.ts +94 -78
- package/src/apps/data/routes/zones.test.ts +2 -2
- package/src/apps/data/routes/zones.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +25 -3
- package/src/apps/funding/routes/deposit-addresses.ts +17 -12
- package/src/apps/funding/routes/quotes.test.ts +5 -9
- package/src/apps/funding/routes/transfers.test.ts +395 -8
- package/src/apps/funding/routes/transfers.ts +76 -59
- package/src/apps/management/routes/api-keys.test.ts +220 -0
- package/src/apps/management/routes/api-keys.ts +280 -3
- package/src/db/Db.ts +13 -1
- package/src/db/Schema.ts +5 -2
- package/src/db/tables/earnVaults.test.ts +9 -9
- package/src/db/tables/fundingCatalog.test.ts +16 -2
- package/src/db/tables/fundingCatalog.ts +9 -3
- package/src/handlers/internal/utils.test.ts +53 -0
- package/src/handlers/internal/utils.ts +15 -0
- package/src/internal/Auth.ts +12 -4
- package/src/internal/EarnVaults.capabilities.test.ts +2 -2
- package/src/internal/EarnVaults.test.ts +1 -1
- package/src/internal/EarnVaults.ts +3 -3
- package/src/internal/Fetch.test.ts +46 -0
- package/src/internal/Fetch.ts +56 -0
- package/src/internal/Log.test.ts +21 -0
- package/src/internal/Log.ts +20 -5
- package/src/internal/OpenApi.ts +14 -4
- package/src/internal/Response.ts +1 -1
- package/src/internal/Schema.test.ts +2 -0
- package/src/internal/Schema.ts +12 -2
- package/src/internal/Store.test.ts +178 -0
- package/src/internal/Store.ts +104 -18
- package/src/internal/Tidx.test.ts +2 -1
- package/src/internal/Tidx.ts +7 -2
- package/src/internal/Timeout.test.ts +35 -0
- package/src/internal/Timeout.ts +73 -0
- package/src/internal/Timing.ts +16 -0
- package/src/internal/WebhookDestination.ts +3 -0
- package/src/internal/funding/Action.test.ts +41 -1
- package/src/internal/funding/Action.ts +69 -41
- package/src/internal/funding/Catalog.test.ts +34 -0
- package/src/internal/funding/Catalog.ts +28 -11
- package/src/internal/funding/Provider.test.ts +73 -0
- package/src/internal/funding/Provider.ts +82 -28
- package/src/internal/funding/Transfer.ts +133 -110
- package/src/internal/funding/providers/index.test.ts +4 -167
- package/src/internal/funding/providers/relay.test.ts +2 -2
- package/src/internal/funding/providers/stargate.test.ts +163 -0
- package/src/internal/funding/providers/stargate.ts +359 -134
- package/src/internal/index.test.ts +1 -0
- package/src/internal/index.ts +1 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HonoRequest } from 'hono/request'
|
|
2
2
|
import { Store } from 'tapimo'
|
|
3
3
|
import * as TestStore from '../../test/Store.js'
|
|
4
|
+
import * as Timeout from './Timeout.js'
|
|
4
5
|
|
|
5
6
|
describe('from', () => {
|
|
6
7
|
const ops = {
|
|
@@ -808,6 +809,183 @@ describe('memoize', () => {
|
|
|
808
809
|
`)
|
|
809
810
|
})
|
|
810
811
|
|
|
812
|
+
test('hands cache persistence to waitUntil without retaining the resolved flight', async () => {
|
|
813
|
+
const pending = deferred<void>()
|
|
814
|
+
const waited: Promise<unknown>[] = []
|
|
815
|
+
const source = Store.memory()
|
|
816
|
+
const store = Store.withRequest(
|
|
817
|
+
Store.from({
|
|
818
|
+
...source,
|
|
819
|
+
put: () => pending.promise,
|
|
820
|
+
type: 'cache',
|
|
821
|
+
}),
|
|
822
|
+
new HonoRequest(new Request('https://api.tempo.xyz')),
|
|
823
|
+
{ waitUntil: (promise) => void waited.push(promise) },
|
|
824
|
+
)
|
|
825
|
+
let calls = 0
|
|
826
|
+
const options = { key: 'memo:stalled-write', store, ttl: 60_000 }
|
|
827
|
+
|
|
828
|
+
const first = await Store.memoize(async () => ({ value: ++calls }), options)
|
|
829
|
+
const second = await Store.memoize(async () => ({ value: ++calls }), options)
|
|
830
|
+
|
|
831
|
+
expect(first).toEqual({ value: 1 })
|
|
832
|
+
expect(second).toEqual({ value: 2 })
|
|
833
|
+
expect(calls).toBe(2)
|
|
834
|
+
expect(waited).toHaveLength(2)
|
|
835
|
+
|
|
836
|
+
pending.resolve()
|
|
837
|
+
await Promise.all(waited)
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
test('ignores cache persistence failures without waitUntil', async () => {
|
|
841
|
+
const store = Store.from({
|
|
842
|
+
...Store.memory(),
|
|
843
|
+
async put() {
|
|
844
|
+
throw new Error('cache unavailable')
|
|
845
|
+
},
|
|
846
|
+
type: 'cache',
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
await expect(
|
|
850
|
+
Store.memoize(async () => ({ value: 'fresh' }), {
|
|
851
|
+
key: 'memo:failed-write',
|
|
852
|
+
store,
|
|
853
|
+
ttl: 60_000,
|
|
854
|
+
}),
|
|
855
|
+
).resolves.toEqual({ value: 'fresh' })
|
|
856
|
+
})
|
|
857
|
+
|
|
858
|
+
test('releases a no-cache flight after deletion fails', async () => {
|
|
859
|
+
let deletes = 0
|
|
860
|
+
let fetches = 0
|
|
861
|
+
const source = Store.from({
|
|
862
|
+
...Store.memory(),
|
|
863
|
+
async delete() {
|
|
864
|
+
if (++deletes === 1) throw new Error('cache unavailable')
|
|
865
|
+
},
|
|
866
|
+
})
|
|
867
|
+
const store = Store.withRequest(
|
|
868
|
+
source,
|
|
869
|
+
new HonoRequest(
|
|
870
|
+
new Request('https://api.tempo.xyz', { headers: { 'cache-control': 'no-cache' } }),
|
|
871
|
+
),
|
|
872
|
+
)
|
|
873
|
+
const options = { key: 'memo:failed-delete', store, ttl: 60_000 }
|
|
874
|
+
|
|
875
|
+
await expect(Store.memoize(async () => ({ value: ++fetches }), options)).rejects.toThrow(
|
|
876
|
+
'cache unavailable',
|
|
877
|
+
)
|
|
878
|
+
await expect(Store.memoize(async () => ({ value: ++fetches }), options)).resolves.toEqual({
|
|
879
|
+
value: 1,
|
|
880
|
+
})
|
|
881
|
+
expect(deletes).toBe(2)
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
test('times out and evicts a stalled cache-miss flight', async () => {
|
|
885
|
+
vi.useFakeTimers()
|
|
886
|
+
try {
|
|
887
|
+
const store = Store.memory()
|
|
888
|
+
const stalled = deferred<{ value: string }>()
|
|
889
|
+
let calls = 0
|
|
890
|
+
let leaderSignal: AbortSignal | undefined
|
|
891
|
+
const options = {
|
|
892
|
+
flightTimeout: 100,
|
|
893
|
+
key: 'memo:test',
|
|
894
|
+
store,
|
|
895
|
+
ttl: 60_000,
|
|
896
|
+
}
|
|
897
|
+
const leader = Store.memoize(async (signal) => {
|
|
898
|
+
calls += 1
|
|
899
|
+
leaderSignal = signal
|
|
900
|
+
return stalled.promise
|
|
901
|
+
}, options)
|
|
902
|
+
await vi.advanceTimersByTimeAsync(0)
|
|
903
|
+
const follower = Store.memoize(async () => {
|
|
904
|
+
calls += 1
|
|
905
|
+
return { value: 'unexpected' }
|
|
906
|
+
}, options)
|
|
907
|
+
const failures = Promise.all([
|
|
908
|
+
expect(leader).rejects.toBeInstanceOf(Store.MemoizeTimeoutError),
|
|
909
|
+
expect(follower).rejects.toBeInstanceOf(Store.MemoizeTimeoutError),
|
|
910
|
+
])
|
|
911
|
+
|
|
912
|
+
await vi.advanceTimersByTimeAsync(100)
|
|
913
|
+
await failures
|
|
914
|
+
|
|
915
|
+
expect(calls).toBe(1)
|
|
916
|
+
expect(leaderSignal?.aborted).toBe(true)
|
|
917
|
+
expect(leaderSignal?.reason).toMatchObject({ name: 'TimeoutError' })
|
|
918
|
+
|
|
919
|
+
const recovered = await Store.memoize(async () => {
|
|
920
|
+
calls += 1
|
|
921
|
+
return { value: 'fresh' }
|
|
922
|
+
}, options)
|
|
923
|
+
stalled.resolve({ value: 'stale' })
|
|
924
|
+
await stalled.promise
|
|
925
|
+
await Promise.resolve()
|
|
926
|
+
|
|
927
|
+
expect(recovered).toEqual({ value: 'fresh' })
|
|
928
|
+
expect(calls).toBe(2)
|
|
929
|
+
expect(await store.get('memo:test')).toBe('tempo-api:store:json:v2:{"value":"fresh"}')
|
|
930
|
+
} finally {
|
|
931
|
+
vi.useRealTimers()
|
|
932
|
+
}
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
test('retains a timed-out flight until a cache mutation settles', async () => {
|
|
936
|
+
vi.useFakeTimers()
|
|
937
|
+
try {
|
|
938
|
+
const source = Store.memory()
|
|
939
|
+
const store = Store.withRequest(
|
|
940
|
+
source,
|
|
941
|
+
new HonoRequest(
|
|
942
|
+
new Request('https://api.tempo.xyz', {
|
|
943
|
+
headers: { 'cache-control': 'no-cache' },
|
|
944
|
+
}),
|
|
945
|
+
),
|
|
946
|
+
)
|
|
947
|
+
let calls = 0
|
|
948
|
+
const options = { flightTimeout: 100, key: 'memo:mutation', store, ttl: 60_000 }
|
|
949
|
+
const leader = Store.memoize(async () => ({ value: ++calls }), options)
|
|
950
|
+
const leaderFailure = expect(leader).rejects.toBeInstanceOf(Store.MemoizeTimeoutError)
|
|
951
|
+
|
|
952
|
+
// The first microtask enters the real in-memory store's async delete.
|
|
953
|
+
await Promise.resolve()
|
|
954
|
+
vi.advanceTimersByTime(100)
|
|
955
|
+
const followerFailure = expect(
|
|
956
|
+
Store.memoize(async () => ({ value: ++calls }), options),
|
|
957
|
+
).rejects.toBeInstanceOf(Store.MemoizeTimeoutError)
|
|
958
|
+
|
|
959
|
+
await Promise.all([leaderFailure, followerFailure])
|
|
960
|
+
expect(calls).toBe(0)
|
|
961
|
+
await expect(Store.memoize(async () => ({ value: ++calls }), options)).resolves.toEqual({
|
|
962
|
+
value: 1,
|
|
963
|
+
})
|
|
964
|
+
} finally {
|
|
965
|
+
vi.useRealTimers()
|
|
966
|
+
}
|
|
967
|
+
})
|
|
968
|
+
|
|
969
|
+
test('defaults cache-miss flights to the API deadline', async () => {
|
|
970
|
+
vi.useFakeTimers()
|
|
971
|
+
try {
|
|
972
|
+
const store = Store.memory()
|
|
973
|
+
const stalled = deferred<{ value: string }>()
|
|
974
|
+
const failure = expect(
|
|
975
|
+
Store.memoize(() => stalled.promise, {
|
|
976
|
+
key: 'memo:default-timeout',
|
|
977
|
+
store,
|
|
978
|
+
ttl: 60_000,
|
|
979
|
+
}),
|
|
980
|
+
).rejects.toBeInstanceOf(Store.MemoizeTimeoutError)
|
|
981
|
+
|
|
982
|
+
await vi.advanceTimersByTimeAsync(Timeout.duration)
|
|
983
|
+
await failure
|
|
984
|
+
} finally {
|
|
985
|
+
vi.useRealTimers()
|
|
986
|
+
}
|
|
987
|
+
})
|
|
988
|
+
|
|
811
989
|
test('returns cached JSON values until the entry expires', async () => {
|
|
812
990
|
const store = Store.memory()
|
|
813
991
|
let calls = 0
|
package/src/internal/Store.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Json } from 'ox'
|
|
2
2
|
|
|
3
|
+
import * as Timeout from './Timeout.js'
|
|
4
|
+
|
|
3
5
|
/** Key-to-value map used by typed stores. */
|
|
4
6
|
export type StoreItemMap = Record<string, unknown>
|
|
5
7
|
|
|
@@ -576,7 +578,11 @@ export declare namespace durableObject {
|
|
|
576
578
|
}
|
|
577
579
|
|
|
578
580
|
/** Associates request cache directives with a store used by request handlers. */
|
|
579
|
-
export function withRequest(
|
|
581
|
+
export function withRequest(
|
|
582
|
+
store: Store,
|
|
583
|
+
request: withRequest.Request,
|
|
584
|
+
options: withRequest.Options = {},
|
|
585
|
+
): Store {
|
|
580
586
|
const source = requestStores.get(store)?.store ?? store
|
|
581
587
|
const scoped = {
|
|
582
588
|
delete: source.delete.bind(source),
|
|
@@ -588,13 +594,18 @@ export function withRequest(store: Store, request: withRequest.Request): Store {
|
|
|
588
594
|
...(source.swap ? { swap: source.swap.bind(source) } : {}),
|
|
589
595
|
type: source.type,
|
|
590
596
|
}
|
|
591
|
-
requestStores.set(scoped, { request, store: source })
|
|
597
|
+
requestStores.set(scoped, { request, store: source, waitUntil: options.waitUntil })
|
|
592
598
|
return scoped
|
|
593
599
|
}
|
|
594
600
|
|
|
595
601
|
export declare namespace withRequest {
|
|
602
|
+
/** Request-scoped cache behavior. */
|
|
603
|
+
export type Options = {
|
|
604
|
+
/** Keeps asynchronous cache persistence alive after the response returns. */
|
|
605
|
+
waitUntil?: ((promise: Promise<unknown>) => void) | undefined
|
|
606
|
+
}
|
|
596
607
|
/** Request surface needed to read cache directives. */
|
|
597
|
-
type Request = {
|
|
608
|
+
export type Request = {
|
|
598
609
|
/** Reads one request header. */
|
|
599
610
|
header(name: string): string | undefined
|
|
600
611
|
}
|
|
@@ -602,16 +613,19 @@ export declare namespace withRequest {
|
|
|
602
613
|
|
|
603
614
|
/**
|
|
604
615
|
* Returns a cached value when present. Otherwise, runs `fetch` and stores its
|
|
605
|
-
* result unless undefined or rejected by `shouldCache`.
|
|
616
|
+
* result unless undefined or rejected by `shouldCache`. Cache persistence is
|
|
617
|
+
* best-effort and never delays the fetched value.
|
|
606
618
|
*/
|
|
607
619
|
export async function memoize<value>(
|
|
608
|
-
fetch: () => Promise<value>,
|
|
620
|
+
fetch: (signal?: AbortSignal) => Promise<value>,
|
|
609
621
|
options: memoize.Options<value>,
|
|
610
622
|
): Promise<value> {
|
|
611
623
|
const scoped = requestStores.get(options.store)
|
|
612
624
|
const { key, ttl } = options
|
|
625
|
+
const flightTimeout = options.flightTimeout ?? Timeout.duration
|
|
613
626
|
const request = scoped?.request
|
|
614
627
|
const store = scoped?.store ?? options.store
|
|
628
|
+
const waitUntil = scoped?.waitUntil
|
|
615
629
|
|
|
616
630
|
// Request cache directives follow the same semantics as the response cache:
|
|
617
631
|
// no-cache refreshes the entry, while no-store bypasses it entirely.
|
|
@@ -619,7 +633,11 @@ export async function memoize<value>(
|
|
|
619
633
|
const directives = (cacheControl ?? request?.header('pragma') ?? '').toLowerCase()
|
|
620
634
|
const noStore = directives.includes('no-store')
|
|
621
635
|
const noCache = noStore || directives.includes('no-cache')
|
|
622
|
-
if (noStore)
|
|
636
|
+
if (noStore)
|
|
637
|
+
return Timeout.run((signal) => fetch(signal), {
|
|
638
|
+
error: new MemoizeTimeoutError(flightTimeout),
|
|
639
|
+
timeout: flightTimeout,
|
|
640
|
+
})
|
|
623
641
|
|
|
624
642
|
if (!noCache) {
|
|
625
643
|
const raw = await store.get(key)
|
|
@@ -641,25 +659,77 @@ export async function memoize<value>(
|
|
|
641
659
|
// Coalesce concurrent misses for this process. The store remains the cache
|
|
642
660
|
// boundary; this only prevents a local cold-key stampede while the first
|
|
643
661
|
// fetch is still in flight.
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
662
|
+
let mutating = noCache
|
|
663
|
+
let settled = false
|
|
664
|
+
const work = async (signal?: AbortSignal) => {
|
|
665
|
+
if (noCache) {
|
|
666
|
+
try {
|
|
667
|
+
await store.delete(key)
|
|
668
|
+
} finally {
|
|
669
|
+
mutating = false
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
signal?.throwIfAborted()
|
|
673
|
+
mutating = false
|
|
674
|
+
const value = await fetch(signal)
|
|
675
|
+
signal?.throwIfAborted()
|
|
647
676
|
if (value === undefined || options.shouldCache?.(value) === false) return value
|
|
648
|
-
|
|
677
|
+
mutating = true
|
|
678
|
+
if (store.type === 'cache') {
|
|
679
|
+
// A lossy cache write cannot turn a successful origin read into an error.
|
|
680
|
+
const persist = Promise.resolve()
|
|
681
|
+
.then(() => store.put(key, jsonEncode(value), { ttl }))
|
|
682
|
+
.catch(() => {})
|
|
683
|
+
if (waitUntil)
|
|
684
|
+
try {
|
|
685
|
+
waitUntil(persist)
|
|
686
|
+
mutating = false
|
|
687
|
+
return value
|
|
688
|
+
} catch {}
|
|
689
|
+
try {
|
|
690
|
+
await persist
|
|
691
|
+
} finally {
|
|
692
|
+
mutating = false
|
|
693
|
+
}
|
|
694
|
+
return value
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
await store.put(key, jsonEncode(value), { ttl })
|
|
698
|
+
} finally {
|
|
699
|
+
mutating = false
|
|
700
|
+
}
|
|
649
701
|
return value
|
|
650
|
-
})()
|
|
651
|
-
flights.set(key, flight)
|
|
652
|
-
|
|
653
|
-
try {
|
|
654
|
-
return await flight
|
|
655
|
-
} finally {
|
|
656
|
-
flights.delete(key)
|
|
657
702
|
}
|
|
703
|
+
let flight!: Promise<value>
|
|
704
|
+
const release = () => {
|
|
705
|
+
// A canceled fetch cannot mutate the cache after its next abort check.
|
|
706
|
+
// Non-cancelable cache mutations retain ownership until they settle.
|
|
707
|
+
if ((settled || !mutating) && flights.get(key) === flight) flights.delete(key)
|
|
708
|
+
}
|
|
709
|
+
flight = Timeout.run(
|
|
710
|
+
async (signal) => {
|
|
711
|
+
signal.addEventListener('abort', release, { once: true })
|
|
712
|
+
try {
|
|
713
|
+
return await work(signal)
|
|
714
|
+
} finally {
|
|
715
|
+
settled = true
|
|
716
|
+
release()
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
error: new MemoizeTimeoutError(flightTimeout),
|
|
721
|
+
timeout: flightTimeout,
|
|
722
|
+
},
|
|
723
|
+
)
|
|
724
|
+
flights.set(key, flight)
|
|
725
|
+
return flight
|
|
658
726
|
}
|
|
659
727
|
|
|
660
728
|
export declare namespace memoize {
|
|
661
729
|
/** Options for {@link memoize}. */
|
|
662
730
|
type Options<value = unknown> = {
|
|
731
|
+
/** Maximum shared cache-miss flight duration in milliseconds. Defaults to 15 seconds. */
|
|
732
|
+
flightTimeout?: number | undefined
|
|
663
733
|
/** Cache key. Callers are responsible for namespacing/versioning. */
|
|
664
734
|
key: string
|
|
665
735
|
/** Returns whether a fetched value should be stored. Single-flight callers still share it. */
|
|
@@ -671,7 +741,14 @@ export declare namespace memoize {
|
|
|
671
741
|
}
|
|
672
742
|
}
|
|
673
743
|
|
|
674
|
-
const requestStores = new WeakMap<
|
|
744
|
+
const requestStores = new WeakMap<
|
|
745
|
+
Store,
|
|
746
|
+
{
|
|
747
|
+
request: withRequest.Request
|
|
748
|
+
store: Store
|
|
749
|
+
waitUntil?: ((promise: Promise<unknown>) => void) | undefined
|
|
750
|
+
}
|
|
751
|
+
>()
|
|
675
752
|
const flights = new WeakMap<Store, Map<string, Promise<unknown>>>()
|
|
676
753
|
|
|
677
754
|
function flightsFor(store: Store) {
|
|
@@ -869,6 +946,15 @@ function isDurableObjectValue(value: unknown): value is DurableObjectValue {
|
|
|
869
946
|
)
|
|
870
947
|
}
|
|
871
948
|
|
|
949
|
+
/** Thrown when a shared memoized cache-miss flight exceeds its configured deadline. */
|
|
950
|
+
export class MemoizeTimeoutError extends Error {
|
|
951
|
+
override name = 'Store.MemoizeTimeoutError'
|
|
952
|
+
|
|
953
|
+
constructor(timeout: number) {
|
|
954
|
+
super(`Memoized cache-miss flight timed out after ${timeout}ms.`)
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
872
958
|
/** Thrown when {@link update} exhausts its compare-and-swap attempts. */
|
|
873
959
|
export class UpdateContentionError extends Error {
|
|
874
960
|
override name = 'Store.UpdateContentionError'
|
|
@@ -231,7 +231,8 @@ describe('withTransientRetry', () => {
|
|
|
231
231
|
|
|
232
232
|
await client.fetch({ chainId: 42431, query: 'SELECT 1' })
|
|
233
233
|
|
|
234
|
-
|
|
234
|
+
const { signal: _signal, ...options } = fetch.mock.calls[0]![0]
|
|
235
|
+
expect(options).toMatchInlineSnapshot(`
|
|
235
236
|
{
|
|
236
237
|
"chainId": 42431,
|
|
237
238
|
"query": "SELECT 1",
|
package/src/internal/Tidx.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type * as Auth from './Auth.js'
|
|
|
5
5
|
import * as Credential from './Credential.js'
|
|
6
6
|
import * as Errors from './Errors.js'
|
|
7
7
|
import type * as Log from './Log.js'
|
|
8
|
+
import * as Timeout from './Timeout.js'
|
|
8
9
|
import * as Viem from './Viem.js'
|
|
9
10
|
|
|
10
11
|
/** Default Tempo indexer (TIDX) base URLs. */
|
|
@@ -184,17 +185,21 @@ export function withTransientRetry(client: Client): Client {
|
|
|
184
185
|
return {
|
|
185
186
|
...client,
|
|
186
187
|
fetch: async (options) => {
|
|
188
|
+
const signal = Timeout.signal({ signal: options.signal })
|
|
187
189
|
let lastError: unknown
|
|
188
190
|
for (let attempt = 0; attempt <= transientRetryCount; attempt++) {
|
|
189
191
|
try {
|
|
190
192
|
// `retryCount: 1` caps the inner client at a single HTTP attempt so
|
|
191
193
|
// the combined budget stays this wrapper's alone.
|
|
192
|
-
return await fetch({ ...options, retryCount: 1 })
|
|
194
|
+
return await fetch({ ...options, retryCount: 1, signal })
|
|
193
195
|
} catch (error) {
|
|
194
196
|
if (!isTransientError(error)) throw error
|
|
195
197
|
lastError = error
|
|
196
198
|
if (attempt < transientRetryCount)
|
|
197
|
-
await
|
|
199
|
+
await Timeout.wait({
|
|
200
|
+
milliseconds: transientRetryDelay * 2 ** attempt,
|
|
201
|
+
signal,
|
|
202
|
+
})
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
throw lastError
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Timeout from './Timeout.js'
|
|
2
|
+
|
|
3
|
+
describe('duration', () => {
|
|
4
|
+
test('uses a 15 second default', () => {
|
|
5
|
+
expect(Timeout.duration).toBe(15_000)
|
|
6
|
+
})
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
describe('signal', () => {
|
|
10
|
+
test('preserves a caller signal and otherwise creates a deadline', () => {
|
|
11
|
+
const caller = new AbortController().signal
|
|
12
|
+
|
|
13
|
+
expect(Timeout.signal({ signal: caller })).toBe(caller)
|
|
14
|
+
expect(Timeout.signal()).toBeInstanceOf(AbortSignal)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('wait', () => {
|
|
19
|
+
test('rejects with the abort reason and clears its timer', async () => {
|
|
20
|
+
vi.useFakeTimers()
|
|
21
|
+
try {
|
|
22
|
+
const controller = new AbortController()
|
|
23
|
+
const reason = new DOMException('deadline exceeded', 'TimeoutError')
|
|
24
|
+
const waiting = Timeout.wait({ milliseconds: 1_000, signal: controller.signal })
|
|
25
|
+
const rejection = expect(waiting).rejects.toBe(reason)
|
|
26
|
+
|
|
27
|
+
controller.abort(reason)
|
|
28
|
+
|
|
29
|
+
await rejection
|
|
30
|
+
expect(vi.getTimerCount()).toBe(0)
|
|
31
|
+
} finally {
|
|
32
|
+
vi.useRealTimers()
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Default maximum duration for API dependency work. */
|
|
2
|
+
export const duration = 15_000
|
|
3
|
+
|
|
4
|
+
/** Returns a caller signal or a signal carrying the default deadline. */
|
|
5
|
+
export function signal(options: signal.Options = {}): AbortSignal {
|
|
6
|
+
return options.signal ?? AbortSignal.timeout(options.timeout ?? duration)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export declare namespace signal {
|
|
10
|
+
/** Options for {@link signal}. */
|
|
11
|
+
type Options = {
|
|
12
|
+
/** Existing caller signal. */
|
|
13
|
+
signal?: AbortSignal | null | undefined
|
|
14
|
+
/** Default maximum duration in milliseconds. */
|
|
15
|
+
timeout?: number | undefined
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Waits for a duration, rejecting immediately when `signal` aborts. */
|
|
20
|
+
export function wait(options: wait.Options): Promise<void> {
|
|
21
|
+
options.signal.throwIfAborted()
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const abort = () => {
|
|
24
|
+
clearTimeout(timer)
|
|
25
|
+
reject(options.signal.reason)
|
|
26
|
+
}
|
|
27
|
+
const timer = setTimeout(() => {
|
|
28
|
+
options.signal.removeEventListener('abort', abort)
|
|
29
|
+
resolve()
|
|
30
|
+
}, options.milliseconds)
|
|
31
|
+
options.signal.addEventListener('abort', abort, { once: true })
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export declare namespace wait {
|
|
36
|
+
/** Options for {@link wait}. */
|
|
37
|
+
type Options = {
|
|
38
|
+
/** Duration to wait in milliseconds. */
|
|
39
|
+
milliseconds: number
|
|
40
|
+
/** Signal that cancels the wait. */
|
|
41
|
+
signal: AbortSignal
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Runs asynchronous work under a hard deadline and aborts cooperative work. */
|
|
46
|
+
export function run<value>(
|
|
47
|
+
work: (signal: AbortSignal) => Promise<value>,
|
|
48
|
+
options: run.Options = {},
|
|
49
|
+
): Promise<value> {
|
|
50
|
+
const timeout = options.timeout ?? duration
|
|
51
|
+
const controller = new AbortController()
|
|
52
|
+
const timeoutError = new DOMException(`Timed out after ${timeout}ms.`, 'TimeoutError')
|
|
53
|
+
const error = options.error ?? timeoutError
|
|
54
|
+
const timer = setTimeout(() => controller.abort(timeoutError), timeout)
|
|
55
|
+
const operation = Promise.resolve().then(() => work(controller.signal))
|
|
56
|
+
|
|
57
|
+
return Promise.race([
|
|
58
|
+
operation,
|
|
59
|
+
new Promise<never>((_, reject) => {
|
|
60
|
+
controller.signal.addEventListener('abort', () => reject(error), { once: true })
|
|
61
|
+
}),
|
|
62
|
+
]).finally(() => clearTimeout(timer))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export declare namespace run {
|
|
66
|
+
/** Options for {@link run}. */
|
|
67
|
+
type Options = {
|
|
68
|
+
/** Error returned to the caller when the deadline expires. */
|
|
69
|
+
error?: Error | undefined
|
|
70
|
+
/** Maximum duration in milliseconds. */
|
|
71
|
+
timeout?: number | undefined
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/internal/Timing.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Context, MiddlewareHandler } from 'hono'
|
|
|
2
2
|
|
|
3
3
|
/** Hono variables used to collect Server-Timing metrics. */
|
|
4
4
|
export type Variables = {
|
|
5
|
+
/** Operations that have started but not finished. */
|
|
6
|
+
activeTimings?: Active[] | undefined
|
|
5
7
|
/** Metrics emitted in the `Server-Timing` response header. */
|
|
6
8
|
serverTiming?: Metric[] | undefined
|
|
7
9
|
}
|
|
@@ -19,12 +21,22 @@ export type Metric = {
|
|
|
19
21
|
name: string
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
/** One operation that has started but not finished. */
|
|
25
|
+
export type Active = {
|
|
26
|
+
/** Metric name. */
|
|
27
|
+
name: string
|
|
28
|
+
/** Monotonic start time. */
|
|
29
|
+
start: number
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
/** Adds a `Server-Timing` header with total request duration. */
|
|
23
33
|
export function middleware<
|
|
24
34
|
environment extends Environment = Environment,
|
|
25
35
|
>(): MiddlewareHandler<environment> {
|
|
26
36
|
return async (c, next) => {
|
|
37
|
+
const active: Active[] = []
|
|
27
38
|
const metrics: Metric[] = []
|
|
39
|
+
c.set('activeTimings', active)
|
|
28
40
|
c.set('serverTiming', metrics)
|
|
29
41
|
|
|
30
42
|
const start = performance.now()
|
|
@@ -44,9 +56,13 @@ export async function time<environment extends Environment, value>(
|
|
|
44
56
|
fn: () => Promise<value> | value,
|
|
45
57
|
) {
|
|
46
58
|
const start = performance.now()
|
|
59
|
+
const active = { name, start }
|
|
60
|
+
c.get('activeTimings')?.push(active)
|
|
47
61
|
try {
|
|
48
62
|
return await fn()
|
|
49
63
|
} finally {
|
|
64
|
+
const activeTimings = c.get('activeTimings')
|
|
65
|
+
if (activeTimings) activeTimings.splice(activeTimings.indexOf(active), 1)
|
|
50
66
|
c.get('serverTiming')?.push({ duration: performance.now() - start, name })
|
|
51
67
|
}
|
|
52
68
|
}
|
|
@@ -176,6 +176,9 @@ export async function send(options: send.Options): Promise<Result> {
|
|
|
176
176
|
signal: controller.signal,
|
|
177
177
|
})
|
|
178
178
|
const durationMs = clock() - start
|
|
179
|
+
// The body is never read; an unreleased stream holds its buffer and
|
|
180
|
+
// connection in the isolate until GC, which stacks up during drains.
|
|
181
|
+
response.body?.cancel().catch(() => {})
|
|
179
182
|
if (
|
|
180
183
|
options.rejectRedirects &&
|
|
181
184
|
(response.status === 0 || (response.status >= 300 && response.status < 400))
|
|
@@ -132,7 +132,47 @@ describe('validateEvmCalls', () => {
|
|
|
132
132
|
sourceTokenAddress: sourceToken,
|
|
133
133
|
}),
|
|
134
134
|
).toThrowErrorMatchingInlineSnapshot(
|
|
135
|
-
`[Funding.Action.InvalidActionError:
|
|
135
|
+
`[Funding.Action.InvalidActionError: Native value must match a validated provider fee.]`,
|
|
136
136
|
)
|
|
137
137
|
})
|
|
138
|
+
|
|
139
|
+
test('accepts an exact provider-validated native value', () => {
|
|
140
|
+
Action.validateEvmCalls({
|
|
141
|
+
action: {
|
|
142
|
+
calls: [approveCall(1_000_000n), { ...depositCall, value: '0x10' }],
|
|
143
|
+
type: 'evm:calls',
|
|
144
|
+
},
|
|
145
|
+
nativeValues: [{ to: spender, value: '0x10' }],
|
|
146
|
+
sourceAmount: '1000000',
|
|
147
|
+
sourceTokenAddress: sourceToken,
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('rejects malformed native value allowances', () => {
|
|
152
|
+
expect(() =>
|
|
153
|
+
Action.validateEvmCalls({
|
|
154
|
+
action: { calls: [approveCall(1_000_000n), depositCall], type: 'evm:calls' },
|
|
155
|
+
nativeValues: [{ to: spender, value: '10' }],
|
|
156
|
+
sourceAmount: '1000000',
|
|
157
|
+
sourceTokenAddress: sourceToken,
|
|
158
|
+
}),
|
|
159
|
+
).toThrowErrorMatchingInlineSnapshot(
|
|
160
|
+
`[Funding.Action.InvalidActionError: Malformed native value allowance.]`,
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('rejects native values outside the exact provider allowance', () => {
|
|
165
|
+
for (const call of [
|
|
166
|
+
{ ...depositCall, to: `0x${'99'.repeat(20)}`, value: '0x10' },
|
|
167
|
+
{ ...depositCall, value: '0x11' },
|
|
168
|
+
])
|
|
169
|
+
expect(() =>
|
|
170
|
+
Action.validateEvmCalls({
|
|
171
|
+
action: { calls: [approveCall(1_000_000n), call], type: 'evm:calls' },
|
|
172
|
+
nativeValues: [{ to: spender, value: '0x10' }],
|
|
173
|
+
sourceAmount: '1000000',
|
|
174
|
+
sourceTokenAddress: sourceToken,
|
|
175
|
+
}),
|
|
176
|
+
).toThrow(Action.InvalidActionError)
|
|
177
|
+
})
|
|
138
178
|
})
|