forked from koshikraj/ottopus
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscorer.test.ts
More file actions
209 lines (187 loc) · 8.32 KB
/
Copy pathscorer.test.ts
File metadata and controls
209 lines (187 loc) · 8.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
import { describe, expect, it } from 'vitest'
import type { TransferIntent } from './intent.js'
import {
type RouteFacts,
type WalletCandidate,
candidateName,
disqualify,
resolveTransferWallet,
transferIneligibility,
} from './scorer.js'
const CHAIN = 'eip155:8453'
const USDC = { symbol: 'USDC', decimals: 6 }
const ETH = { symbol: 'ETH', decimals: 18 }
const wallet = (n: number, over: Partial<WalletCandidate> = {}): WalletCandidate => ({
walletId: `w${n}`,
account: `${CHAIN}:0x${n.toString(16).padStart(40, '0')}`,
label: null,
canSign: true,
assetBalance: 1_000_000_000n, // 1000 USDC
gasBalance: 10n ** 16n,
...over,
})
const usdcIntent: TransferIntent = {
kind: 'transfer',
asset: `${CHAIN}/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913`,
amount: '500000000',
to: `${CHAIN}:0xd8da6bf26964af9d7eed9e03e53415d37aa96045`,
}
const ethIntent: TransferIntent = { ...usdcIntent, asset: `${CHAIN}/slip44:60`, amount: '1000000000000000000' }
describe('disqualifiers beat price', () => {
const clean: RouteFacts = { verifiedTargets: true, simulationOk: true, unlimitedApproval: false, supportedCapability: true }
it('passes a clean route', () => {
expect(disqualify(clean)).toBeNull()
})
it.each([
['an unverified contract', { verifiedTargets: false }, /no verified source/],
['a failed simulation', { simulationOk: false }, /simulation failed/],
['an unlimited approval', { unlimitedApproval: true }, /unlimited approval/],
['an unsupported capability', { supportedCapability: false }, /cannot sign/],
['a stale quote', { quoteExpiresAt: '2020-01-01T00:00:00Z' }, /quote has expired/],
] as const)('ends a route over %s', (_, facts, reason) => {
expect(disqualify({ ...clean, ...facts })).toMatch(reason)
})
it('does not treat "no simulation ran" as a failure', () => {
expect(disqualify({ ...clean, simulationOk: null })).toBeNull()
})
})
describe('transfer eligibility', () => {
it('refuses a watch-only wallet before anything else', () => {
expect(transferIneligibility(wallet(1, { canSign: false, assetBalance: 0n }), usdcIntent, USDC, false)).toMatch(/watch-only/)
})
it('needs enough of the token, and says how short', () => {
expect(transferIneligibility(wallet(1, { assetBalance: 12_000_000n }), usdcIntent, USDC, false)).toBe(
'holds only 12 USDC on Base, short of 500 USDC',
)
expect(transferIneligibility(wallet(1, { assetBalance: 0n }), usdcIntent, USDC, false)).toBe('holds no USDC on Base')
})
it('needs something for gas on a token transfer', () => {
expect(transferIneligibility(wallet(1, { gasBalance: 0n }), usdcIntent, USDC, false)).toMatch(/pay gas/)
})
it('needs strictly more than the amount for a native transfer, to leave gas', () => {
const exact = wallet(1, { assetBalance: 10n ** 18n, gasBalance: 10n ** 18n })
expect(transferIneligibility(exact, ethIntent, ETH, true)).toMatch(/still pay gas/)
const more = wallet(1, { assetBalance: 10n ** 18n + 1n, gasBalance: 10n ** 18n + 1n })
expect(transferIneligibility(more, ethIntent, ETH, true)).toBeNull()
})
})
describe('resolveTransferWallet', () => {
it('recommends the one eligible wallet, and says why the others lost', () => {
const out = resolveTransferWallet({
intent: usdcIntent,
candidates: [wallet(1, { label: 'Main' }), wallet(2, { assetBalance: 0n }), wallet(3, { canSign: false })],
asset: USDC,
native: false,
})
expect(out.ok).toBe(true)
if (!out.ok) return
expect(out.walletId).toBe('w1')
expect(out.resolution.account).toEqual({ caip10: wallet(1).account, label: 'Main' })
expect(out.resolution.reason).toBe('Recommended Main (…0001) because it holds 1,000 USDC on Base, enough to send 500 USDC, has gas.')
expect(out.resolution.candidatesConsidered.map((c) => c.reason)).toEqual([
'holds no USDC on Base',
'is watch-only and cannot sign',
])
})
it('breaks a tie by balance, and keeps a vault out of everyday sends', () => {
const out = resolveTransferWallet({
intent: usdcIntent,
candidates: [
wallet(1, { label: 'Daily', assetBalance: 600_000_000n }),
wallet(2, { label: 'Vault', assetBalance: 50_000_000_000n }),
wallet(3, { label: 'Trading', assetBalance: 900_000_000n }),
],
asset: USDC,
native: false,
})
if (!out.ok) throw new Error(out.reasons.join())
expect(out.walletId).toBe('w3')
expect(out.resolution.reason).toMatch(/of 3 wallets that could, it holds the most/)
const reasons = Object.fromEntries(out.resolution.candidatesConsidered.map((c) => [c.label, c.reason]))
expect(reasons.Vault).toMatch(/labelled Vault, which reads as not for everyday sends/)
expect(reasons.Daily).toBe('holds 600 USDC on Base, less than Trading (…0003)')
})
/** 1 ETH + 1 wei against 2 ETH. A modulus tie-break got this wrong once. */
it('compares whole balances, not a wrapped slice of them', () => {
const out = resolveTransferWallet({
intent: ethIntent,
candidates: [
wallet(1, { label: 'One', assetBalance: 10n ** 18n + 1n, gasBalance: 10n ** 18n + 1n }),
wallet(2, { label: 'Two', assetBalance: 2n * 10n ** 18n, gasBalance: 2n * 10n ** 18n }),
],
asset: ETH,
native: true,
})
if (!out.ok) throw new Error(out.reasons.join())
expect(out.walletId).toBe('w2')
expect(out.resolution.reason).toMatch(/holds 2 ETH on Base/)
expect(out.resolution.candidatesConsidered[0]!.reason).toBe('holds 1 ETH on Base, less than Two (…0002)')
})
it('lets a penalty outweigh any balance', () => {
const out = resolveTransferWallet({
intent: usdcIntent,
candidates: [
wallet(1, { label: 'Daily', assetBalance: 600_000_000n }),
wallet(2, { label: 'Vault', assetBalance: 10n ** 30n }),
],
asset: USDC,
native: false,
})
if (!out.ok) throw new Error(out.reasons.join())
expect(out.walletId).toBe('w1')
})
it('honours a chosen wallet and only checks it can', () => {
const chosen = wallet(2, { label: 'Second', assetBalance: 700_000_000n })
const out = resolveTransferWallet({
intent: { ...usdcIntent, fromAccount: chosen.account },
candidates: [wallet(1, { label: 'Main', assetBalance: 50_000_000_000n }), chosen],
asset: USDC,
native: false,
})
if (!out.ok) throw new Error(out.reasons.join())
expect(out.walletId).toBe('w2')
expect(out.resolution.reason).toBe('You chose Second (…0002). It holds 700 USDC on Base and has gas.')
expect(out.resolution.candidatesConsidered[0]!.reason).toMatch(/you chose the wallet/)
})
it('refuses a chosen wallet that cannot, with the reason', () => {
const chosen = wallet(2, { assetBalance: 1_000_000n })
const out = resolveTransferWallet({
intent: { ...usdcIntent, fromAccount: chosen.account },
candidates: [wallet(1), chosen],
asset: USDC,
native: false,
})
expect(out).toEqual({ ok: false, reasons: ['the wallet ending …0002 holds only 1 USDC on Base, short of 500 USDC'] })
})
it('refuses a chosen account that is not linked', () => {
const out = resolveTransferWallet({
intent: { ...usdcIntent, fromAccount: `${CHAIN}:0x9999999999999999999999999999999999999999` },
candidates: [wallet(1)],
asset: USDC,
native: false,
})
expect(!out.ok && out.reasons[0]).toMatch(/is not a linked wallet on Base/)
})
it('says why every wallet lost when none can', () => {
const out = resolveTransferWallet({
intent: usdcIntent,
candidates: [wallet(1, { label: 'Main', assetBalance: 0n }), wallet(2, { canSign: false })],
asset: USDC,
native: false,
})
expect(out).toEqual({
ok: false,
reasons: ['Main (…0001) holds no USDC on Base', 'the wallet ending …0002 is watch-only and cannot sign'],
})
})
it('names the chain when nothing is linked there at all', () => {
const out = resolveTransferWallet({ intent: usdcIntent, candidates: [], asset: USDC, native: false })
expect(out).toEqual({ ok: false, reasons: ['no linked wallet is on Base'] })
})
})
describe('candidateName', () => {
it('uses the label when there is one', () => {
expect(candidateName({ label: 'Main', account: wallet(1).account })).toBe('Main (…0001)')
expect(candidateName({ label: null, account: wallet(1).account })).toBe('the wallet ending …0001')
})
})