@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 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,2 @@
1
+ export const API_URL = "https://dev-api.coinvoyage.io"
2
+ // export const API_URL = "http://localhost:8000"
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./config"
2
+ export * from "./fetcher"
3
+ export * from "./pay-order"
@@ -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,3 @@
1
+ export function capitalize(str: string) {
2
+ return str.charAt(0).toUpperCase() + str.slice(1)
3
+ }
@@ -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,5 @@
1
+ import { Currency } from "./model";
2
+
3
+ export type SettlementCurrency = {
4
+ receiving_address: string;
5
+ } & Currency
@@ -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,3 @@
1
+ export function shortenAddress(address: string, length = 4): string {
2
+ return `${address.slice(0, 2 + length)}…${address.slice(-length)}`
3
+ }
@@ -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 }
@@ -0,0 +1,3 @@
1
+ export * from "./account"
2
+ export * from "./browser"
3
+ export * from "./plurar"
@@ -0,0 +1,5 @@
1
+ export const withPlural = (totalQuantity: number, singular: string, plural: string) => {
2
+ if (totalQuantity == 1) return `${totalQuantity} ${singular}`;
3
+ else return `${totalQuantity} ${plural}`;
4
+ };
5
+