@coin-voyage/shared 0.0.1
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/package.json +37 -0
- package/src/api/config.ts +2 -0
- package/src/api/fetcher.ts +38 -0
- package/src/api/index.ts +3 -0
- package/src/api/pay-order.ts +349 -0
- package/src/common/assert.ts +21 -0
- package/src/common/chainExplorer.ts +27 -0
- package/src/common/chains.ts +130 -0
- package/src/common/currencies.ts +42 -0
- package/src/common/debug.ts +11 -0
- package/src/common/format.ts +3 -0
- package/src/common/i18n/index.ts +18 -0
- package/src/common/i18n/languages/en.ts +38 -0
- package/src/common/i18n/languages/es.ts +38 -0
- package/src/common/index.ts +12 -0
- package/src/common/model.ts +43 -0
- package/src/common/organization.ts +5 -0
- package/src/common/pay.ts +194 -0
- package/src/common/retryBackoff.ts +24 -0
- package/src/common/time.ts +78 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-is-mobile.ts +16 -0
- package/src/utils/account.ts +3 -0
- package/src/utils/browser.ts +24 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/plurar.ts +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coin-voyage/shared",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./*": [
|
|
9
|
+
"./src/*/index.ts",
|
|
10
|
+
"./src/*/index.tsx"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"typesVersions": {
|
|
17
|
+
"*": {
|
|
18
|
+
"*": [
|
|
19
|
+
"src/*/index.d.ts"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"detect-browser": "^5.3.0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"viem": "^2.22.8",
|
|
28
|
+
"zod": "^3.22.4"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc --build --force",
|
|
32
|
+
"release:build": "pnpm build",
|
|
33
|
+
"clean": "rm -rf node_modules",
|
|
34
|
+
"type-check": "tsc --noEmit",
|
|
35
|
+
"test": "vitest run"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { API_URL } from "./config";
|
|
2
|
+
|
|
3
|
+
type FetchApiOptions = {
|
|
4
|
+
path: string;
|
|
5
|
+
options?: RequestInit;
|
|
6
|
+
throwOnFailure?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function fetchApi<T>(opts: FetchApiOptions): Promise<T | undefined> {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(`${API_URL}/${opts.path}`, {
|
|
12
|
+
...opts.options,
|
|
13
|
+
headers: {
|
|
14
|
+
...opts.options?.headers,
|
|
15
|
+
"Content-Type": "application/json",
|
|
16
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
const text = await response.text();
|
|
21
|
+
console.error(
|
|
22
|
+
JSON.stringify({
|
|
23
|
+
path: opts.path,
|
|
24
|
+
status: response.status,
|
|
25
|
+
statusText: response.statusText,
|
|
26
|
+
details: text
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
return await response.json();
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
console.error(`path: ${opts.path}, message: ${error.message}`);
|
|
34
|
+
if (opts.throwOnFailure) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ChainId, ChainType, CurrencyAmount, CurrencyBase, CurrencyWithAmount, CurrencyWithBalance, PayOrder, PayOrderMetadata, PayOrderMode, PayOrderStatus, zDepositPayOrder, zPayOrderMetadata } from "../common/index";
|
|
3
|
+
import { fetchApi } from "./fetcher";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fetches a PayOrder by its ID.
|
|
7
|
+
*
|
|
8
|
+
* Retrieves a PayOrder object from the API using the provided payOrderId.
|
|
9
|
+
* This function requires an API key for authentication.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} payOrderId - The unique identifier of the PayOrder.
|
|
12
|
+
* @param {string} apiKey - The API key required for authentication.
|
|
13
|
+
* @returns {Promise<PayOrder | undefined>} - The PayOrder object if the request is successful.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const apiKey = 'yourApiKey';
|
|
17
|
+
* const payOrder = await getPayOrder('123456', apiKey);
|
|
18
|
+
*/
|
|
19
|
+
export async function getPayOrder(payOrderId: string, apiKey: string): Promise<PayOrder | undefined> {
|
|
20
|
+
return fetchApi<PayOrder>({
|
|
21
|
+
path: `pay-orders/${payOrderId}`,
|
|
22
|
+
options: {
|
|
23
|
+
headers: {
|
|
24
|
+
"X-API-KEY": apiKey
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** PayOrder parameters. The payOrder is created only after user taps pay. */
|
|
31
|
+
export interface DepositPayOrderParams {
|
|
32
|
+
/**
|
|
33
|
+
* Metadata to attach to the payOrder.
|
|
34
|
+
*/
|
|
35
|
+
metadata?: PayOrderMetadata
|
|
36
|
+
/**
|
|
37
|
+
* Value in USD received from the Deposit.
|
|
38
|
+
*/
|
|
39
|
+
destination_value_usd?: number
|
|
40
|
+
/**
|
|
41
|
+
* Desired output token + chain.
|
|
42
|
+
*/
|
|
43
|
+
destination_currency: CurrencyBase
|
|
44
|
+
/**
|
|
45
|
+
* Output amount to deposit.
|
|
46
|
+
*/
|
|
47
|
+
destination_amount?: string
|
|
48
|
+
/**
|
|
49
|
+
* Receiving address for the deposit.
|
|
50
|
+
*/
|
|
51
|
+
receiving_address: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a `DEPOSIT` type PayOrder.
|
|
56
|
+
*
|
|
57
|
+
* Generates a PayOrder of type `DEPOSIT` with the provided parameters.
|
|
58
|
+
* This function ensures the request parameters are valid using a schema validation.
|
|
59
|
+
*
|
|
60
|
+
* @param {DepositPayOrderParams} createOrderParams - Parameters required to create a deposit PayOrder.
|
|
61
|
+
* @param {string} apiKey - The API key required for authentication.
|
|
62
|
+
* @param {boolean} [throwOnFailure] - Whether to throw an error if the request fails.
|
|
63
|
+
* @returns {Promise<PayOrder | undefined>} - The PayOrder object if the request is successful.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* const apiKey = 'yourApiKey';
|
|
67
|
+
* const depositOrderParams = {
|
|
68
|
+
* amount: 1000,
|
|
69
|
+
* currency: 'USD',
|
|
70
|
+
* metadata: { description: 'Deposit for account' }
|
|
71
|
+
* };
|
|
72
|
+
* const depositPayOrder = await createDepositPayOrder(depositOrderParams, apiKey);
|
|
73
|
+
*/
|
|
74
|
+
export async function createDepositPayOrder(createOrderParams: DepositPayOrderParams, apiKey: string, throwOnFailure?: boolean): Promise<PayOrder | undefined> {
|
|
75
|
+
const result = zDepositPayOrder.safeParse(createOrderParams);
|
|
76
|
+
if (!result.success) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return fetchApi<PayOrder>({
|
|
81
|
+
path: `pay-orders`, options: {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
mode: PayOrderMode.DEPOSIT,
|
|
85
|
+
...createOrderParams
|
|
86
|
+
}),
|
|
87
|
+
headers: {
|
|
88
|
+
"X-API-KEY": apiKey
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
throwOnFailure: throwOnFailure
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type SalePayOrderParams = {
|
|
96
|
+
/**
|
|
97
|
+
* Metadata to attach to the payOrder.
|
|
98
|
+
*/
|
|
99
|
+
metadata?: PayOrderMetadata
|
|
100
|
+
/**
|
|
101
|
+
* Expected value in USD to be received from the Sale.
|
|
102
|
+
*/
|
|
103
|
+
destination_value_usd: number
|
|
104
|
+
/**
|
|
105
|
+
* Expected token on what chain to receive the value from the Sale.
|
|
106
|
+
*/
|
|
107
|
+
destination_currency?: CurrencyBase
|
|
108
|
+
/**
|
|
109
|
+
* Expected receiving address to receive the value from the Sale.
|
|
110
|
+
*/
|
|
111
|
+
receiving_address?: string
|
|
112
|
+
} | {
|
|
113
|
+
/**
|
|
114
|
+
* Metadata to attach to the payOrder.
|
|
115
|
+
*/
|
|
116
|
+
metadata?: PayOrderMetadata
|
|
117
|
+
/**
|
|
118
|
+
* Expected token on what chain to receive the value from the Sale.
|
|
119
|
+
*/
|
|
120
|
+
destination_currency: CurrencyBase
|
|
121
|
+
/**
|
|
122
|
+
* Expected amount to be received from the Sale.
|
|
123
|
+
*/
|
|
124
|
+
destination_amount: string
|
|
125
|
+
/**
|
|
126
|
+
* Expected receiving address to receive the value from the Sale.
|
|
127
|
+
*/
|
|
128
|
+
receiving_address?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generates a `sale` type PayOrder.
|
|
134
|
+
*
|
|
135
|
+
* Creates a `sale` type PayOrder with a fixed valueOut `destination_value_usd`.
|
|
136
|
+
* This function should be executed
|
|
137
|
+
*
|
|
138
|
+
* and a SHA-512 hash signature of the concatenated API key, secret, and timestamp.
|
|
139
|
+
*
|
|
140
|
+
* @param {SalePayOrderParams} payOrderParams - destination_value_usd and metadata for this payOrder.
|
|
141
|
+
* @param {string} signature - signature generated by the `generateAuthorizationSignature` function.
|
|
142
|
+
* @returns {PayOrder | undefined} - A PayOrder object if the request was successful.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* const apiKey = 'yourApiKey';
|
|
146
|
+
* const apiSecret = 'yourApiSecret';
|
|
147
|
+
* const authSignature = generateAuthorizationSignature(apiKey, apiSecret);
|
|
148
|
+
* const payOrder = await createSalePayOrder(
|
|
149
|
+
* createSalePayOrder({
|
|
150
|
+
* destination_value_usd: 200,
|
|
151
|
+
* metadata: {
|
|
152
|
+
* items: [
|
|
153
|
+
* {
|
|
154
|
+
* name: "t-shirt",
|
|
155
|
+
* description: "a nice t-shirt",
|
|
156
|
+
* image: "https://example.com/tshirt.jpg",
|
|
157
|
+
* quantity: 1,
|
|
158
|
+
* unit_price: 200,
|
|
159
|
+
* currency: "USD"
|
|
160
|
+
* }
|
|
161
|
+
* ]
|
|
162
|
+
* }}, authSignature)
|
|
163
|
+
*/
|
|
164
|
+
export async function createSalePayOrder(createOrderParams: SalePayOrderParams, signature: string): Promise<PayOrder | undefined> {
|
|
165
|
+
try {
|
|
166
|
+
if (createOrderParams?.metadata) {
|
|
167
|
+
zPayOrderMetadata.parse(createOrderParams.metadata);
|
|
168
|
+
}
|
|
169
|
+
return fetchApi<PayOrder>({
|
|
170
|
+
path: `pay-orders`, options: {
|
|
171
|
+
method: "POST",
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
mode: PayOrderMode.SALE,
|
|
174
|
+
...createOrderParams
|
|
175
|
+
}),
|
|
176
|
+
headers: {
|
|
177
|
+
"Authorization": signature
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err instanceof z.ZodError) {
|
|
184
|
+
throw new Error(err.errors.map(e => e.message).join(", "))
|
|
185
|
+
}
|
|
186
|
+
throw err
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export type PayOrderQuoteParams = {
|
|
191
|
+
wallet_address: string
|
|
192
|
+
chain_type: ChainType,
|
|
193
|
+
chain_id?: ChainId
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generates a PayOrder Quote.
|
|
198
|
+
*
|
|
199
|
+
* Creates a PayOrder Quote for an existing PayOrder by providing wallet information and chain details.
|
|
200
|
+
* This function requires an API key for authentication.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} orderId - The unique identifier of the PayOrder for which the quote is requested.
|
|
203
|
+
* @param {PayOrderQuoteParams} quoteParams - Contains `wallet_address`, `chain_type`, chain_id`.
|
|
204
|
+
* @param {string} apiKey - The API key for authorization.
|
|
205
|
+
* @returns {CurrencyWithBalance[] | undefined} - An array of PayTokens if the request was successful.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* const orderId = 'existingOrderId';
|
|
209
|
+
* const apiKey = 'yourApiKey';
|
|
210
|
+
* const quoteParams = {
|
|
211
|
+
* wallet_address: '0x1234...abcd',
|
|
212
|
+
* chain_type: ChainType.EVM,
|
|
213
|
+
* chain_id: 1 // Ethereum Mainnet
|
|
214
|
+
* };
|
|
215
|
+
*
|
|
216
|
+
* const payOrderQuote = await payOrderQuote(orderId, quoteParams, apiKey);
|
|
217
|
+
|
|
218
|
+
*/
|
|
219
|
+
export async function payOrderQuote(orderId: string, quoteParams: PayOrderQuoteParams, apiKey: string): Promise<CurrencyWithBalance[] | undefined> {
|
|
220
|
+
return fetchApi<CurrencyWithBalance[]>({
|
|
221
|
+
path: `pay-orders/${orderId}/quote`, options: {
|
|
222
|
+
method: "POST",
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
...quoteParams
|
|
225
|
+
}),
|
|
226
|
+
headers: {
|
|
227
|
+
"X-API-KEY": apiKey
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
export type PaymentDetailsParams = {
|
|
235
|
+
payOrderId: string
|
|
236
|
+
outTokenAddress?: string
|
|
237
|
+
outChainId: ChainId
|
|
238
|
+
refundAddress: string
|
|
239
|
+
apiKey: string
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type PaymentDetails = {
|
|
243
|
+
payorder_id: string
|
|
244
|
+
status: PayOrderStatus
|
|
245
|
+
expires_at: Date
|
|
246
|
+
|
|
247
|
+
refund_address: string
|
|
248
|
+
deposit_address: string
|
|
249
|
+
receiving_address?: string
|
|
250
|
+
|
|
251
|
+
source_currency: CurrencyWithAmount
|
|
252
|
+
source_amount: CurrencyAmount
|
|
253
|
+
destination_currency?: CurrencyWithAmount
|
|
254
|
+
destination_amount?: CurrencyAmount
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Retrieves payment details for a specific PayOrder.
|
|
259
|
+
*
|
|
260
|
+
* This function fetches payment details associated with a specific PayOrder ID.
|
|
261
|
+
* It allows specifying the token and blockchain network (via `outTokenAddress` and `outChainId`)
|
|
262
|
+
* to retrieve source currency information, as well as a refund address for the transaction.
|
|
263
|
+
*
|
|
264
|
+
* The request is authenticated using the provided API key in the headers.
|
|
265
|
+
*
|
|
266
|
+
* @param {PaymentDetailsParams} params - Parameters to retrieve the payment details.
|
|
267
|
+
* @param {string} params.payOrderId - The unique identifier of the PayOrder for which payment details are fetched.
|
|
268
|
+
* @param {string} [params.outTokenAddress] - (Optional) The token address of the source currency.
|
|
269
|
+
* @param {ChainId} params.outChainId - The blockchain network ID where the token resides.
|
|
270
|
+
* @param {string} params.refundAddress - The address where funds will be refunded in case of a failed transaction.
|
|
271
|
+
* @param {string} params.apiKey - The API key used to authenticate the request.
|
|
272
|
+
*
|
|
273
|
+
* @returns {Promise<PaymentDetails | undefined>} - The payment details object if the request is successful.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* const paymentDetails = await payOrderPaymentDetails({
|
|
277
|
+
* payOrderId: '12345',
|
|
278
|
+
* outTokenAddress: '0x1234567890abcdef1234567890abcdef12345678',
|
|
279
|
+
* outChainId: 1,
|
|
280
|
+
* refundAddress: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
|
|
281
|
+
* apiKey: 'yourApiKey'
|
|
282
|
+
* });
|
|
283
|
+
*
|
|
284
|
+
* console.log(paymentDetails);
|
|
285
|
+
*/
|
|
286
|
+
export async function payOrderPaymentDetails({
|
|
287
|
+
payOrderId,
|
|
288
|
+
outTokenAddress,
|
|
289
|
+
outChainId,
|
|
290
|
+
refundAddress,
|
|
291
|
+
apiKey
|
|
292
|
+
}: PaymentDetailsParams): Promise<PaymentDetails | undefined> {
|
|
293
|
+
return fetchApi<PaymentDetails>({
|
|
294
|
+
path: `pay-orders/${payOrderId}/payment-details`, options: {
|
|
295
|
+
method: "POST",
|
|
296
|
+
body: JSON.stringify({
|
|
297
|
+
source_currency: {
|
|
298
|
+
address: outTokenAddress,
|
|
299
|
+
chain_id: outChainId
|
|
300
|
+
},
|
|
301
|
+
refund_address: refundAddress
|
|
302
|
+
}),
|
|
303
|
+
headers: {
|
|
304
|
+
"X-API-KEY": apiKey
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Processes a PayOrder transaction.
|
|
312
|
+
*
|
|
313
|
+
* This function triggers the processing of a PayOrder by providing the transaction hash
|
|
314
|
+
* that represents the payment on the blockchain. The request is authenticated using an API key.
|
|
315
|
+
*
|
|
316
|
+
* @param {Object} params - Parameters required to process the PayOrder.
|
|
317
|
+
* @param {string} params.payOrderId - The unique identifier of the PayOrder to be processed.
|
|
318
|
+
* @param {string} params.sourceTransactionHash - The transaction hash representing the payment on the blockchain.
|
|
319
|
+
* @param {string} params.apiKey - The API key used to authenticate the request.
|
|
320
|
+
*
|
|
321
|
+
* @returns {Promise<void>}
|
|
322
|
+
*
|
|
323
|
+
* @example
|
|
324
|
+
* const processedPayment = await processPayOrder({
|
|
325
|
+
* payOrderId: '12345',
|
|
326
|
+
* sourceTransactionHash: '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd',
|
|
327
|
+
* apiKey: 'yourApiKey'
|
|
328
|
+
* });
|
|
329
|
+
*
|
|
330
|
+
* console.log(processedPayment);
|
|
331
|
+
*/
|
|
332
|
+
export async function processPayOrder({
|
|
333
|
+
payOrderId,
|
|
334
|
+
sourceTransactionHash,
|
|
335
|
+
apiKey
|
|
336
|
+
}: {
|
|
337
|
+
payOrderId: string
|
|
338
|
+
sourceTransactionHash: string
|
|
339
|
+
apiKey: string
|
|
340
|
+
}): Promise<void> {
|
|
341
|
+
return fetchApi<void>({
|
|
342
|
+
path: `pay-orders/${payOrderId}/process?tx_hash=${sourceTransactionHash}`, options: {
|
|
343
|
+
method: "GET",
|
|
344
|
+
headers: {
|
|
345
|
+
"X-API-KEY": apiKey
|
|
346
|
+
},
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { debugJson } from "./debug"
|
|
2
|
+
|
|
3
|
+
export function assert(condition: boolean, ...args: any[]): asserts condition {
|
|
4
|
+
if (!condition) {
|
|
5
|
+
throw new Error(
|
|
6
|
+
`Assertion failed: ${args.map((a) => debugJson(a)).join(", ")}`
|
|
7
|
+
)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function assertNotNull<T>(
|
|
12
|
+
value: T | null | undefined,
|
|
13
|
+
...args: any[]
|
|
14
|
+
): T {
|
|
15
|
+
assert(value !== null && value !== undefined, ...args)
|
|
16
|
+
return value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function assertEqual<T>(a: T, b: T, ...args: any[]): void {
|
|
20
|
+
assert(a === b, ...args)
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ChainId } from "./chains"
|
|
2
|
+
|
|
3
|
+
const chainExplorerUrls: Record<ChainId, string> = {
|
|
4
|
+
[ChainId.ETH]: "https://etherscan.io",
|
|
5
|
+
[ChainId.ARB]: "https://arbiscan.io",
|
|
6
|
+
[ChainId.BASE]: "https://basescan.org",
|
|
7
|
+
[ChainId.BLAST]: "https://blastexplorer.io",
|
|
8
|
+
[ChainId.OP]: "https://optimistic.etherscan.io",
|
|
9
|
+
[ChainId.POL]: "https://polygonscan.com",
|
|
10
|
+
[ChainId.AVAX]: "https://snowtrace.io",
|
|
11
|
+
[ChainId.SEPOLIA]: "https://sepolia.etherscan.io",
|
|
12
|
+
[ChainId.SEPOLIA_BASE]: "https://sepolia.basescan.org",
|
|
13
|
+
[ChainId.SEPOLIA_ARB]: "https://sepolia.arbiscan.io",
|
|
14
|
+
[ChainId.SEPOLIA_OP]: "https://sepolia-optimism.etherscan.io",
|
|
15
|
+
[ChainId.BSC]: "https://bscscan.com",
|
|
16
|
+
[ChainId.ZKSYNC]: "https://explorer.zksync.io",
|
|
17
|
+
[ChainId.FTM]: "https://ftmscan.com",
|
|
18
|
+
[ChainId.SOL]: "https://solscan.io",
|
|
19
|
+
[ChainId.SUI]: "https://suiscan.xyz",
|
|
20
|
+
[ChainId.TRX]: "https://tronscan.org",
|
|
21
|
+
[ChainId.BTC]: "https://mempool.space",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getChainExplorerTxUrl(chainId: ChainId, txHash?: string) {
|
|
25
|
+
const explorer = chainExplorerUrls[chainId]
|
|
26
|
+
return `${explorer}/tx/${txHash}`
|
|
27
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import * as chains from "viem/chains"
|
|
2
|
+
|
|
3
|
+
export enum ChainType {
|
|
4
|
+
EVM = "EVM",
|
|
5
|
+
// Solana virtual machine
|
|
6
|
+
SOL = "SOL",
|
|
7
|
+
SUI = "SUI",
|
|
8
|
+
TRON = "TRON",
|
|
9
|
+
// Unspent transaction output (e.g. Bitcoin, Litecoin, Dogecoin)
|
|
10
|
+
UTXO = "UTXO",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getChainTypeName(chainType?: ChainType): string {
|
|
14
|
+
switch (chainType) {
|
|
15
|
+
case ChainType.EVM:
|
|
16
|
+
return "Ethereum"
|
|
17
|
+
case ChainType.SOL:
|
|
18
|
+
return "Solana"
|
|
19
|
+
case ChainType.SUI:
|
|
20
|
+
return "Sui"
|
|
21
|
+
case ChainType.TRON:
|
|
22
|
+
return "Tron"
|
|
23
|
+
case ChainType.UTXO:
|
|
24
|
+
return "Bitcoin"
|
|
25
|
+
default:
|
|
26
|
+
return ""
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export enum ChainId {
|
|
31
|
+
ETH = 1,
|
|
32
|
+
OP = 10,
|
|
33
|
+
BSC = 56,
|
|
34
|
+
POL = 137,
|
|
35
|
+
FTM = 250,
|
|
36
|
+
ZKSYNC = 324,
|
|
37
|
+
BASE = 8453,
|
|
38
|
+
ARB = 42161,
|
|
39
|
+
AVAX = 43114,
|
|
40
|
+
BLAST = 81457,
|
|
41
|
+
|
|
42
|
+
// testnet
|
|
43
|
+
SEPOLIA = 11155111,
|
|
44
|
+
SEPOLIA_BASE = 84532,
|
|
45
|
+
SEPOLIA_ARB = 421614,
|
|
46
|
+
SEPOLIA_OP = 11155420,
|
|
47
|
+
|
|
48
|
+
// UTXO (IDs are made up)
|
|
49
|
+
BTC = 20000000000001,
|
|
50
|
+
// BCH = 20000000000002,
|
|
51
|
+
// LTC = 20000000000003,
|
|
52
|
+
// DGE = 20000000000004,
|
|
53
|
+
|
|
54
|
+
// None-EVM (IDs are made up)
|
|
55
|
+
SOL = 30000000000001,
|
|
56
|
+
SUI = 30000000000002,
|
|
57
|
+
TRX = 30000000000003,
|
|
58
|
+
// XRPL = 30000000000004,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getChainTypeByChainId(chainId?: ChainId): ChainType {
|
|
62
|
+
switch (chainId) {
|
|
63
|
+
case ChainId.ETH:
|
|
64
|
+
case ChainId.OP:
|
|
65
|
+
case ChainId.BSC:
|
|
66
|
+
case ChainId.POL:
|
|
67
|
+
case ChainId.FTM:
|
|
68
|
+
case ChainId.ZKSYNC:
|
|
69
|
+
case ChainId.BASE:
|
|
70
|
+
case ChainId.ARB:
|
|
71
|
+
case ChainId.AVAX:
|
|
72
|
+
case ChainId.BLAST:
|
|
73
|
+
return ChainType.EVM
|
|
74
|
+
case ChainId.SOL:
|
|
75
|
+
return ChainType.SOL
|
|
76
|
+
case ChainId.SUI:
|
|
77
|
+
return ChainType.SUI
|
|
78
|
+
case ChainId.TRX:
|
|
79
|
+
return ChainType.TRON
|
|
80
|
+
case ChainId.BTC:
|
|
81
|
+
return ChainType.UTXO
|
|
82
|
+
default:
|
|
83
|
+
return ChainType.EVM
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isTestnetChain(chainId: number): boolean {
|
|
88
|
+
return Boolean(
|
|
89
|
+
Object.values(chains).find((chain) => chain.id === chainId)?.testnet
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getChainName(chainId?: ChainId): string | undefined {
|
|
94
|
+
if (!chainId) {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
switch (chainId) {
|
|
99
|
+
case ChainId.ETH:
|
|
100
|
+
return "Ethereum"
|
|
101
|
+
case ChainId.OP:
|
|
102
|
+
return "Optimism"
|
|
103
|
+
case ChainId.BSC:
|
|
104
|
+
return "Binance Smart Chain"
|
|
105
|
+
case ChainId.POL:
|
|
106
|
+
return "Polygon"
|
|
107
|
+
case ChainId.FTM:
|
|
108
|
+
return "Fantom"
|
|
109
|
+
case ChainId.ZKSYNC:
|
|
110
|
+
return "zkSync"
|
|
111
|
+
case ChainId.BASE:
|
|
112
|
+
return "Base"
|
|
113
|
+
case ChainId.ARB:
|
|
114
|
+
return "Arbitrum"
|
|
115
|
+
case ChainId.AVAX:
|
|
116
|
+
return "Avalanche"
|
|
117
|
+
case ChainId.BLAST:
|
|
118
|
+
return "Blast"
|
|
119
|
+
case ChainId.SOL:
|
|
120
|
+
return "Solana"
|
|
121
|
+
case ChainId.SUI:
|
|
122
|
+
return "Sui"
|
|
123
|
+
case ChainId.TRX:
|
|
124
|
+
return "Tron"
|
|
125
|
+
case ChainId.BTC:
|
|
126
|
+
return "Bitcoin"
|
|
127
|
+
default:
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface CurrencyExchangeRate {
|
|
2
|
+
name: string
|
|
3
|
+
symbol: string
|
|
4
|
+
currency: string
|
|
5
|
+
decimals: number
|
|
6
|
+
rateUSD: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const currencyRateUSD: CurrencyExchangeRate = {
|
|
10
|
+
name: "US Dollar",
|
|
11
|
+
symbol: "$",
|
|
12
|
+
currency: "USD",
|
|
13
|
+
decimals: 2,
|
|
14
|
+
rateUSD: 1,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const data: [string, string, string, number][] = [
|
|
18
|
+
["Euro", "€", "EUR", 2],
|
|
19
|
+
["Argentine Peso", "ARS", "ARS", 0],
|
|
20
|
+
["Bolivian Boliviano", "$Bs", "BOB", 0],
|
|
21
|
+
["Turkish Lira", "₺", "TRY", 0],
|
|
22
|
+
["New Taiwan Dollar", "NT$", "TWD", 0],
|
|
23
|
+
["Ukrainian Hryvnia", "₴", "UAH", 0],
|
|
24
|
+
["Naira", "₦", "NGN", 0],
|
|
25
|
+
["Swiss Franc", "₣", "CHF", 2],
|
|
26
|
+
["Japanese Yen", "¥", "JPY", 0],
|
|
27
|
+
["Korean Won", "₩", "KRW", 0],
|
|
28
|
+
["Yuan", "¥", "CNY", 0],
|
|
29
|
+
["Pound", "£", "GBP", 2],
|
|
30
|
+
["Canadian Dollar", "C$", "CAD", 2],
|
|
31
|
+
["Australian Dollar", "A$", "AUD", 2],
|
|
32
|
+
["Singapore Dollar", "S$", "SGD", 2],
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
export const nonUsdCurrencies = data.map(
|
|
36
|
+
([name, symbol, currency, decimals]) => ({
|
|
37
|
+
name,
|
|
38
|
+
symbol,
|
|
39
|
+
currency,
|
|
40
|
+
decimals,
|
|
41
|
+
})
|
|
42
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Return compact JSON, 1000 chars max. Never throws.
|
|
2
|
+
export function debugJson(obj: any) {
|
|
3
|
+
try {
|
|
4
|
+
const serialized = JSON.stringify(obj, (_, value) =>
|
|
5
|
+
typeof value === "bigint" ? value.toString() : value
|
|
6
|
+
)
|
|
7
|
+
return serialized.slice(0, 1000)
|
|
8
|
+
} catch (e: any) {
|
|
9
|
+
return `<JSON error: ${e.message}>`
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { en } from "./languages/en"
|
|
2
|
+
import { es } from "./languages/es"
|
|
3
|
+
|
|
4
|
+
export interface Locale {
|
|
5
|
+
languageCode?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type I18NTranslation = typeof en
|
|
9
|
+
|
|
10
|
+
// Return i18n translation based on locale
|
|
11
|
+
export const i18n = (locale: Locale | undefined): I18NTranslation => {
|
|
12
|
+
switch (locale?.languageCode) {
|
|
13
|
+
case "es":
|
|
14
|
+
return es
|
|
15
|
+
default:
|
|
16
|
+
return en
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const en = {
|
|
2
|
+
// format.ts
|
|
3
|
+
format: {
|
|
4
|
+
fee: () => "Fee:",
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
// op.ts
|
|
8
|
+
op: {
|
|
9
|
+
acceptedInbound: (
|
|
10
|
+
readableAmount: string,
|
|
11
|
+
otherCoinSymbol: string,
|
|
12
|
+
homeCoinSymbol: string,
|
|
13
|
+
chain?: string
|
|
14
|
+
) =>
|
|
15
|
+
`Accepted ${readableAmount} ${otherCoinSymbol}${
|
|
16
|
+
chain ? ` on ${chain}` : ""
|
|
17
|
+
} as ${homeCoinSymbol}`,
|
|
18
|
+
sentOutbound: (
|
|
19
|
+
readableAmount: string,
|
|
20
|
+
coinSymbol: string,
|
|
21
|
+
chain?: string
|
|
22
|
+
) => `Sent ${readableAmount} ${coinSymbol}${chain ? ` on ${chain}` : ""}`,
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// time.ts
|
|
26
|
+
time: {
|
|
27
|
+
soon: () => "soon",
|
|
28
|
+
now: (long?: boolean) => `${long ? "just now" : "now"}`,
|
|
29
|
+
minutesAgo: (minutes: number, long?: boolean) =>
|
|
30
|
+
`${minutes}m ${long ? "ago" : ""}`,
|
|
31
|
+
hoursAgo: (hours: number, long?: boolean) =>
|
|
32
|
+
`${hours}h ${long ? "ago" : ""}`,
|
|
33
|
+
daysAgo: (days: number, long?: boolean) => `${days}d ${long ? "ago" : ""}`,
|
|
34
|
+
inDays: (days: number, long?: boolean) => `${long ? "in" : ""} ${days}d`,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type LanguageDefinition = typeof en
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { LanguageDefinition } from "./en"
|
|
2
|
+
export const es: LanguageDefinition = {
|
|
3
|
+
// format.ts
|
|
4
|
+
format: {
|
|
5
|
+
fee: () => "Tasas:",
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
// op.ts
|
|
9
|
+
op: {
|
|
10
|
+
acceptedInbound: (
|
|
11
|
+
readableAmount: string,
|
|
12
|
+
otherCoinSymbol: string,
|
|
13
|
+
homeCoinSymbol: string,
|
|
14
|
+
chain?: string
|
|
15
|
+
) =>
|
|
16
|
+
`Aceptado ${readableAmount} ${otherCoinSymbol}${
|
|
17
|
+
chain ? ` en ${chain}` : ""
|
|
18
|
+
} como ${homeCoinSymbol}`,
|
|
19
|
+
sentOutbound: (
|
|
20
|
+
readableAmount: string,
|
|
21
|
+
coinSymbol: string,
|
|
22
|
+
chain?: string
|
|
23
|
+
) =>
|
|
24
|
+
`Envidado ${readableAmount} ${coinSymbol}${chain ? ` en ${chain}` : ""}`,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// time.ts
|
|
28
|
+
time: {
|
|
29
|
+
soon: () => "pronto",
|
|
30
|
+
now: () => "ahora",
|
|
31
|
+
minutesAgo: (minutes: number, long?: boolean) =>
|
|
32
|
+
`${long ? "hace" : ""} ${minutes}m`,
|
|
33
|
+
hoursAgo: (hours: number, long?: boolean) =>
|
|
34
|
+
`${long ? "hace" : ""} ${hours}h`,
|
|
35
|
+
daysAgo: (days: number, long?: boolean) => `${long ? "hace" : ""} ${days}d`,
|
|
36
|
+
inDays: (days: number, long?: boolean) => `${long ? "en" : ""} ${days}d`,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./assert"
|
|
2
|
+
export * from "./chainExplorer"
|
|
3
|
+
export * from "./chains"
|
|
4
|
+
export * from "./currencies"
|
|
5
|
+
export * from "./organization"
|
|
6
|
+
export * from "./pay"
|
|
7
|
+
export * from "./debug"
|
|
8
|
+
export * from "./format"
|
|
9
|
+
export * from "./model"
|
|
10
|
+
export * from "./retryBackoff"
|
|
11
|
+
export * from "./time"
|
|
12
|
+
export * from "./i18n"
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Address } from "viem"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { ChainId } from "./chains"
|
|
4
|
+
|
|
5
|
+
export const zSolanaPublicKey = z.string().regex(/^[1-9A-HJ-NP-Za-km-z]+$/)
|
|
6
|
+
|
|
7
|
+
export type SolanaPublicKey = z.infer<typeof zSolanaPublicKey>
|
|
8
|
+
|
|
9
|
+
export interface Currency extends CurrencyBase {
|
|
10
|
+
id?: string
|
|
11
|
+
name: string
|
|
12
|
+
ticker: string
|
|
13
|
+
decimals: number
|
|
14
|
+
image_uri?: string
|
|
15
|
+
price_usd?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CurrencyBase {
|
|
19
|
+
chain_id: ChainId
|
|
20
|
+
address: string | null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const zAddress = z
|
|
24
|
+
.string()
|
|
25
|
+
.regex(/^0x[0-9a-f]{40}$/i)
|
|
26
|
+
.refine((s): s is Address => true)
|
|
27
|
+
|
|
28
|
+
export const zBigIntStr = z
|
|
29
|
+
.string()
|
|
30
|
+
.regex(/^[0-9]+$/i)
|
|
31
|
+
.refine((s): s is BigIntStr => true)
|
|
32
|
+
|
|
33
|
+
export type BigIntStr = `${bigint}`
|
|
34
|
+
|
|
35
|
+
export type PlatformType = "ios" | "android" | "other"
|
|
36
|
+
|
|
37
|
+
export function dateToUnix(d: Date): number {
|
|
38
|
+
return Math.floor(d.getTime() / 1000)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function unixToDate(unix: number): Date {
|
|
42
|
+
return new Date(unix * 1000)
|
|
43
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Address, Hex, zeroAddress } from "viem"
|
|
2
|
+
import z from "zod"
|
|
3
|
+
import { Currency, zAddress, zBigIntStr } from "./model"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export enum PayOrderMode {
|
|
7
|
+
SALE = "SALE", // product or item sale, value out can only be adjusted by the merchant
|
|
8
|
+
DEPOSIT = "DEPOSIT", // deposit into user account, let the user specify the value out
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const zBridgeTokenOutOptions = z.array(
|
|
12
|
+
z.object({
|
|
13
|
+
token: zAddress,
|
|
14
|
+
amount: zBigIntStr.transform((a) => BigInt(a)),
|
|
15
|
+
})
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export type BridgeTokenOutOptions = z.infer<typeof zBridgeTokenOutOptions>
|
|
19
|
+
|
|
20
|
+
// NOTE: be careful to modify this type only in backward-compatible ways.
|
|
21
|
+
// Add OPTIONAL fields, etc. Anything else requires a migration.
|
|
22
|
+
export const zPayOrderMetadata = z.object({
|
|
23
|
+
items: z
|
|
24
|
+
.array(
|
|
25
|
+
z.object({
|
|
26
|
+
name: z.string(),
|
|
27
|
+
description: z.string().optional(),
|
|
28
|
+
image: z.string().optional(),
|
|
29
|
+
quantity: z.number().int().optional(),
|
|
30
|
+
unit_price: z.number().optional(),
|
|
31
|
+
currency: z.string().optional(),
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
.describe("Details about what's being ordered, donated, deposited, etc."),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// also validate not both destination_value_usd and destination_amount are present
|
|
38
|
+
export const zDepositPayOrder = z.object({
|
|
39
|
+
metadata: zPayOrderMetadata.optional(),
|
|
40
|
+
destination_value_usd: z.number().optional()
|
|
41
|
+
.refine((val) => val === undefined || Number(val) > 0, {
|
|
42
|
+
message: "destination_value_usd must be greater than zero if defined",
|
|
43
|
+
}),
|
|
44
|
+
destination_currency: z.object({
|
|
45
|
+
chain_id: z.number(),
|
|
46
|
+
address: z.string().nullable(),
|
|
47
|
+
}),
|
|
48
|
+
destination_amount: z.string().optional()
|
|
49
|
+
.refine((val) => val === undefined || Number(val) > 0, {
|
|
50
|
+
message: "destination_amount must be greater than zero if defined",
|
|
51
|
+
}),
|
|
52
|
+
receiving_address: z.string().min(1),
|
|
53
|
+
}).refine(
|
|
54
|
+
(data) =>
|
|
55
|
+
!(data.destination_value_usd && data.destination_amount), // Ensure not both are present
|
|
56
|
+
{
|
|
57
|
+
message: "Only one of destination_value_usd or destination_amount should be present.",
|
|
58
|
+
path: ["destination_value_usd", "destination_amount"],
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export type PayOrderMetadata = z.infer<typeof zPayOrderMetadata>
|
|
63
|
+
|
|
64
|
+
export enum PayOrderStatus {
|
|
65
|
+
PENDING = "PENDING",
|
|
66
|
+
FAILED = "FAILED",
|
|
67
|
+
AWAITING_PAYMENT = "AWAITING_PAYMENT",
|
|
68
|
+
AWAITING_CONFIRMATION = "AWAITING_CONFIRMATION",
|
|
69
|
+
EXECUTING_ORDER = "EXECUTING_ORDER",
|
|
70
|
+
COMPLETED = "COMPLETED",
|
|
71
|
+
EXPIRED = "EXPIRED",
|
|
72
|
+
REFUNDED = "REFUNDED",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type PayOrder = {
|
|
76
|
+
id: string
|
|
77
|
+
|
|
78
|
+
mode: PayOrderMode.SALE | PayOrderMode.DEPOSIT
|
|
79
|
+
status: PayOrderStatus
|
|
80
|
+
metadata?: PayOrderMetadata
|
|
81
|
+
expires_at?: Date
|
|
82
|
+
|
|
83
|
+
source_currency?: Currency
|
|
84
|
+
source_amount?: CurrencyAmount
|
|
85
|
+
deposit_tx_hash?: string
|
|
86
|
+
|
|
87
|
+
destination_currency?: Currency
|
|
88
|
+
destination_amount?: CurrencyAmount
|
|
89
|
+
receiving_tx_hash?: string
|
|
90
|
+
|
|
91
|
+
receiving_address?: string
|
|
92
|
+
refund_address?: string
|
|
93
|
+
deposit_address?: string
|
|
94
|
+
|
|
95
|
+
refund_tx_hash?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface CurrencyWithAmount extends Currency {
|
|
99
|
+
currency_amount: CurrencyAmount
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface CurrencyWithBalance extends CurrencyWithAmount {
|
|
103
|
+
balance?: CurrencyAmount
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface CurrencyAmount {
|
|
107
|
+
ui_amount: number
|
|
108
|
+
raw_amount: bigint
|
|
109
|
+
value_usd: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type OnChainCall = {
|
|
113
|
+
to: Address
|
|
114
|
+
data: Hex
|
|
115
|
+
value: bigint
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const emptyOnChainCall: OnChainCall = {
|
|
119
|
+
to: zeroAddress,
|
|
120
|
+
data: "0x",
|
|
121
|
+
value: BigInt(0),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const zUUID = z.string().uuid()
|
|
125
|
+
|
|
126
|
+
export type UUID = z.infer<typeof zUUID>
|
|
127
|
+
|
|
128
|
+
export type PaymentCreationErrorEvent = {
|
|
129
|
+
type: "payment_creation_error"
|
|
130
|
+
errorMessage: string
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type PaymentStartedEvent = {
|
|
134
|
+
type: "payment_started"
|
|
135
|
+
paymentId: string
|
|
136
|
+
chainId: number
|
|
137
|
+
txHash: Hex | string | null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type PaymentCompletedEvent = {
|
|
141
|
+
type: "payment_completed"
|
|
142
|
+
paymentId: UUID
|
|
143
|
+
chainId: number
|
|
144
|
+
txHash: Hex | string
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export type PaymentBouncedEvent = {
|
|
148
|
+
type: "payment_bounced"
|
|
149
|
+
paymentId: UUID
|
|
150
|
+
chainId: number
|
|
151
|
+
txHash: Hex | string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type PayEvent =
|
|
155
|
+
| PaymentStartedEvent
|
|
156
|
+
| PaymentCompletedEvent
|
|
157
|
+
| PaymentBouncedEvent
|
|
158
|
+
|
|
159
|
+
export interface WebhookEndpoint {
|
|
160
|
+
id: string
|
|
161
|
+
organization_id: string
|
|
162
|
+
active: boolean
|
|
163
|
+
url: string
|
|
164
|
+
webhook_secret: string
|
|
165
|
+
subscription_events: WebhookEventStatus[]
|
|
166
|
+
created_at: Date
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export enum WebhookEventStatus {
|
|
170
|
+
ORDER_CREATED = "ORDER_CREATED",
|
|
171
|
+
ORDER_AWAITING_PAYMENT = "ORDER_AWAITING_PAYMENT",
|
|
172
|
+
ORDER_CONFIRMING = "ORDER_CONFIRMING",
|
|
173
|
+
ORDER_EXECUTING = "ORDER_EXECUTING",
|
|
174
|
+
ORDER_COMPLETED = "ORDER_COMPLETED",
|
|
175
|
+
ORDER_ERROR = "ORDER_ERROR",
|
|
176
|
+
ORDER_REFUNDED = "ORDER_REFUNDED"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface WebhookEvent {
|
|
180
|
+
id: UUID
|
|
181
|
+
endpoint: WebhookEndpoint
|
|
182
|
+
body: PayEvent
|
|
183
|
+
status: WebhookEventStatus
|
|
184
|
+
//deliveries: WebhookDelivery[]
|
|
185
|
+
createdAt: Date
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// export interface WebhookDelivery {
|
|
189
|
+
// id: UUID
|
|
190
|
+
// eventId: UUID
|
|
191
|
+
// httpStatus: number | null
|
|
192
|
+
// body: string | null
|
|
193
|
+
// createdAt: Date
|
|
194
|
+
// }
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Retries a function up to a maximum number of times, with exponential backoff.
|
|
2
|
+
// Current settings, max total wait time is ~10 seconds.
|
|
3
|
+
export async function retryBackoff<T>(
|
|
4
|
+
name: string,
|
|
5
|
+
fn: () => Promise<T>,
|
|
6
|
+
maxRetries = 5,
|
|
7
|
+
backoffFn: (i: number) => number = (i) => Math.min(2000, 250 * 2 ** i)
|
|
8
|
+
): Promise<T> {
|
|
9
|
+
for (let i = 1; ; i++) {
|
|
10
|
+
try {
|
|
11
|
+
return await fn()
|
|
12
|
+
} catch (e) {
|
|
13
|
+
if (i <= maxRetries) {
|
|
14
|
+
const sleepMs = backoffFn(i)
|
|
15
|
+
await new Promise((r) => setTimeout(r, sleepMs))
|
|
16
|
+
} else {
|
|
17
|
+
console.warn(`[RETRY] ${name} QUITTING after try ${i}, error: ${e}`)
|
|
18
|
+
break
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// TODO: add performance logging
|
|
23
|
+
throw new Error(`too many retries: ${name}`)
|
|
24
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { i18n, Locale } from "./i18n"
|
|
2
|
+
|
|
3
|
+
/** Returns current unix time, in seconds */
|
|
4
|
+
export function now() {
|
|
5
|
+
return Math.floor(Date.now() / 1000)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Returns "now", "1m", "2h", etc. Long form: "just now", "1m ago", ... */
|
|
9
|
+
export function timeAgo(
|
|
10
|
+
sinceS: number,
|
|
11
|
+
locale?: Locale,
|
|
12
|
+
nowS?: number,
|
|
13
|
+
long?: boolean
|
|
14
|
+
): string {
|
|
15
|
+
const i18 = i18n(locale).time
|
|
16
|
+
if (nowS == null) {
|
|
17
|
+
nowS = now()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const seconds = Math.floor(nowS - sinceS)
|
|
21
|
+
if (seconds < 60) {
|
|
22
|
+
return i18.now(long)
|
|
23
|
+
}
|
|
24
|
+
const minutes = Math.floor(seconds / 60)
|
|
25
|
+
if (minutes < 60) {
|
|
26
|
+
return i18.minutesAgo(minutes, long)
|
|
27
|
+
}
|
|
28
|
+
const hours = Math.floor(minutes / 60)
|
|
29
|
+
if (hours < 24) {
|
|
30
|
+
return i18.hoursAgo(hours, long)
|
|
31
|
+
}
|
|
32
|
+
const days = Math.floor(hours / 24)
|
|
33
|
+
return `${days}d${long ? " ago" : ""}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Returns "soon", "1d", "2d", etc. Long form: "in 1d", "in 2d", ... */
|
|
37
|
+
export function daysUntil(
|
|
38
|
+
untilS: number,
|
|
39
|
+
locale?: Locale,
|
|
40
|
+
nowS?: number,
|
|
41
|
+
long?: boolean
|
|
42
|
+
): string {
|
|
43
|
+
const i18 = i18n(locale).time
|
|
44
|
+
if (nowS == null) {
|
|
45
|
+
nowS = now()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const seconds = Math.floor(untilS - nowS)
|
|
49
|
+
const minutes = Math.floor(seconds / 60)
|
|
50
|
+
const hours = Math.floor(minutes / 60)
|
|
51
|
+
const days = Math.floor(hours / 24)
|
|
52
|
+
if (days < 1) {
|
|
53
|
+
return i18.soon()
|
|
54
|
+
}
|
|
55
|
+
return i18.inDays(days, long)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Returns eg "12/11/2023, 10:44" */
|
|
59
|
+
export function timeString(s: number) {
|
|
60
|
+
const date = new Date(s * 1000)
|
|
61
|
+
return date.toLocaleString([], {
|
|
62
|
+
year: "numeric",
|
|
63
|
+
month: "numeric",
|
|
64
|
+
day: "numeric",
|
|
65
|
+
hour: "2-digit",
|
|
66
|
+
minute: "2-digit",
|
|
67
|
+
dayPeriod: "short",
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Returns eg "Aug 2023" */
|
|
72
|
+
export function timeMonth(s: number) {
|
|
73
|
+
const date = new Date(s * 1000)
|
|
74
|
+
return date.toLocaleString([], {
|
|
75
|
+
month: "short",
|
|
76
|
+
year: "numeric",
|
|
77
|
+
})
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useIsMobile } from "./use-is-mobile";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { isMobile } from "../utils/browser"
|
|
3
|
+
|
|
4
|
+
export function useIsMobile() {
|
|
5
|
+
const [mobile, setMobile] = useState(isMobile())
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const handleResize = () => {
|
|
9
|
+
setMobile(isMobile())
|
|
10
|
+
}
|
|
11
|
+
window.addEventListener("resize", handleResize)
|
|
12
|
+
return () => window.removeEventListener("resize", handleResize)
|
|
13
|
+
}, [])
|
|
14
|
+
|
|
15
|
+
return mobile
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { detect } from "detect-browser"
|
|
2
|
+
|
|
3
|
+
const detectBrowser = () => {
|
|
4
|
+
const browser = detect()
|
|
5
|
+
return browser?.name ?? ""
|
|
6
|
+
}
|
|
7
|
+
const detectOS = () => {
|
|
8
|
+
const browser = detect()
|
|
9
|
+
return browser?.os ?? ""
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const isIOS = () => {
|
|
13
|
+
const os = detectOS()
|
|
14
|
+
return os.toLowerCase().includes("ios")
|
|
15
|
+
}
|
|
16
|
+
const isAndroid = () => {
|
|
17
|
+
const os = detectOS()
|
|
18
|
+
return os.toLowerCase().includes("android")
|
|
19
|
+
}
|
|
20
|
+
const isMobile = () => {
|
|
21
|
+
return isAndroid() || isIOS()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { detectBrowser, detectOS, isAndroid, isIOS, isMobile }
|