Skip to content

Commit 2e03949

Browse files
committed
Add CTX gift card purchase paid from an Edge wallet
Orders a card with POST /gift-cards, which allocates a payment address and quotes the crypto amount, then hands that address and amount to the normal send flow and polls the card until CTX reports it fulfilled. The request field names and the order shape are what staging actually accepts; the placeholder gift card cleaner from the read-only pass was a guess and is replaced by the real one. Staging quotes every asset on testnet, so the payment resolves to the one testnet chain Edge carries a wallet for, sepolia.
1 parent e222388 commit 2e03949

9 files changed

Lines changed: 640 additions & 34 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Unreleased (develop)
44

55
- added: App/device attestation for gated info-server requests
6-
- added: CTX spend-api pubkey auth prototype, with the anonymous session readable from the gift card account info scene
6+
- added: CTX spend-api prototype, covering the anonymous pubkey session and a gift card purchase paid from an Edge wallet, both driven from the gift card account info scene
77
- added: "-m" tag on the version number in the Help scene for Maestro test builds
88
- changed: Target Android 16 (API level 36), which Google Play requires for app updates submitted after Aug 30, 2026. Predictive back is opted out of for now, since React Native 0.79 cannot handle it, so the back button behaves exactly as it did before.
99
- changed: Style the entire "Already have an account? Sign in" line in the getting-started USP carousel with the tertiary link color, not just "Sign in".
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { describe, expect, test } from '@jest/globals'
2+
import type { EdgeCurrencyWallet } from 'edge-core-js'
3+
4+
import {
5+
getCtxPaymentNativeAmount,
6+
getCtxPaymentPluginId,
7+
isCtxGiftCardPaid
8+
} from '../plugins/gift-cards/ctxSpendPurchase'
9+
import type { CtxSpendGiftCard } from '../plugins/gift-cards/ctxSpendTypes'
10+
11+
// A card as `POST /gift-cards` actually returns it, trimmed to the fields
12+
// these helpers read.
13+
const makeCard = (
14+
overrides: Partial<CtxSpendGiftCard> = {}
15+
): CtxSpendGiftCard => {
16+
const card: CtxSpendGiftCard = {
17+
id: '2adf8f1c-4885-44e4-a17b-8ae35b9a1bc5',
18+
merchantId: '7c8bf315-703f-4b6c-972d-574411c059e9',
19+
merchantName: 'Amazon',
20+
cardFiatAmount: '0.01',
21+
cardFiatCurrency: 'USD',
22+
paymentId: '99df9dd3-985f-4fa2-a82e-6074222f92cd',
23+
paymentMethod: 'crypto',
24+
paymentCryptoAddress: '0x67587B625a63a1E692eb7D73Dc11d57Ff3597406',
25+
paymentCryptoAmount: '0.000005200000000000',
26+
paymentCryptoChain: 'ETH',
27+
paymentCryptoCurrency: 'ETH',
28+
paymentCryptoNetwork: 'testnet',
29+
paymentUrls: {},
30+
rate: '1923.0769',
31+
status: 'unpaid',
32+
displayStatus: 'unpaid',
33+
paymentStatus: 'unpaid',
34+
fulfilmentStatus: 'pending',
35+
created: '2026-08-17T19:53:14Z',
36+
updated: '2026-08-17T19:53:14Z'
37+
}
38+
return { ...card, ...overrides }
39+
}
40+
41+
const makeWallet = (multiplier: string): EdgeCurrencyWallet =>
42+
({
43+
currencyInfo: { denominations: [{ multiplier }] }
44+
} as unknown as EdgeCurrencyWallet)
45+
46+
describe('getCtxPaymentPluginId', () => {
47+
test('maps a testnet ETH quote to sepolia, the only testnet Edge carries', () => {
48+
expect(getCtxPaymentPluginId(makeCard())).toBe('sepolia')
49+
})
50+
51+
test('maps a mainnet ETH quote to ethereum', () => {
52+
expect(
53+
getCtxPaymentPluginId(makeCard({ paymentCryptoNetwork: 'mainnet' }))
54+
).toBe('ethereum')
55+
})
56+
57+
test('returns undefined for chains Edge has no wallet type for', () => {
58+
// Staging quotes these happily; the app still cannot pay them.
59+
for (const chain of ['XMR', 'ZEC', 'ZANO', 'DASH', 'XLM', 'LTC', 'BCH']) {
60+
expect(
61+
getCtxPaymentPluginId(makeCard({ paymentCryptoChain: chain }))
62+
).toBeUndefined()
63+
}
64+
})
65+
66+
test('returns undefined when the quote names no chain or network', () => {
67+
expect(
68+
getCtxPaymentPluginId(makeCard({ paymentCryptoChain: undefined }))
69+
).toBeUndefined()
70+
expect(
71+
getCtxPaymentPluginId(makeCard({ paymentCryptoNetwork: undefined }))
72+
).toBeUndefined()
73+
})
74+
})
75+
76+
describe('getCtxPaymentNativeAmount', () => {
77+
test('converts the quote to wei exactly', () => {
78+
// The payment URI for this same card carries value=5200000000000.
79+
expect(
80+
getCtxPaymentNativeAmount(makeCard(), makeWallet('1000000000000000000'))
81+
).toBe('5200000000000')
82+
})
83+
84+
test('never rounds down, since an underpayment leaves the card unpaid', () => {
85+
// A quote finer than the chain's smallest unit must round up.
86+
const card = makeCard({ paymentCryptoAmount: '0.000000015' })
87+
expect(getCtxPaymentNativeAmount(card, makeWallet('100000000'))).toBe('2')
88+
})
89+
90+
test('throws when the card carries no payment amount', () => {
91+
expect(() =>
92+
getCtxPaymentNativeAmount(
93+
makeCard({ paymentCryptoAmount: undefined }),
94+
makeWallet('1000000000000000000')
95+
)
96+
).toThrow()
97+
})
98+
})
99+
100+
describe('isCtxGiftCardPaid', () => {
101+
test('a fresh order is not paid', () => {
102+
expect(isCtxGiftCardPaid(makeCard())).toBe(false)
103+
expect(isCtxGiftCardPaid(makeCard({ paymentStatus: 'pending' }))).toBe(
104+
false
105+
)
106+
})
107+
108+
test('recognises the paid state staging actually reports', () => {
109+
// Observed live: `unpaid` becomes `paid` once the send confirms.
110+
expect(isCtxGiftCardPaid(makeCard({ paymentStatus: 'paid' }))).toBe(true)
111+
})
112+
113+
test('an in-progress fulfilment does not make it paid', () => {
114+
// `fulfilmentStatus` runs on its own track (`pending`, then `ordered`)
115+
// and says nothing about whether the payment landed.
116+
expect(isCtxGiftCardPaid(makeCard({ fulfilmentStatus: 'ordered' }))).toBe(
117+
false
118+
)
119+
})
120+
})

0 commit comments

Comments
 (0)