@zebec-network/exchange-card-sdk 1.9.0-dev.1 → 1.9.0-dev.10
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/artifacts/abi/index.js +3 -1
- package/dist/constants.d.ts +2 -1
- package/dist/constants.js +1 -0
- package/dist/services/aleoService.d.ts +54 -28
- package/dist/services/aleoService.js +248 -42
- package/dist/services/cantonService.d.ts +0 -0
- package/dist/services/cantonService.js +103 -0
- package/dist/services/index.d.ts +6 -5
- package/dist/services/index.js +7 -5
- package/dist/utils.d.ts +22 -9
- package/dist/utils.js +24 -10
- package/package.json +8 -4
- package/dist/services/bitcoinService.d.ts +0 -48
- package/dist/services/bitcoinService.js +0 -146
package/dist/constants.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BobaChainId, QuaiChainId } from "./types";
|
|
1
|
+
import type { BobaChainId, QuaiChainId } from "./types";
|
|
2
2
|
export declare const CARD_API_URL: Record<"Production" | "Sandbox", string>;
|
|
3
3
|
export declare const NEAR_RPC_URL: Record<"Production" | "Sandbox", string>;
|
|
4
4
|
export declare const XRPL_RPC_URL: Record<"Production" | "Sandbox", string>;
|
|
@@ -17,6 +17,7 @@ export declare const BITCOIN_ENDPOINTS: {
|
|
|
17
17
|
readonly Sandbox: "https://mempool.space/testnet/api";
|
|
18
18
|
readonly Production: "https://mempool.space/api";
|
|
19
19
|
};
|
|
20
|
+
export declare const ALEO_NETWORK_CLIENT_URL = "https://api.provable.com/v2";
|
|
20
21
|
export declare const BOBA_CHAIN_ID: Record<"mainnet" | "testnet", BobaChainId>;
|
|
21
22
|
export declare const QUAI_CHAIN_ID: Record<"mainnet" | "testnet", QuaiChainId>;
|
|
22
23
|
export declare const DEFAULT_EVM_GAS_LIMIT = 3000000;
|
package/dist/constants.js
CHANGED
|
@@ -35,6 +35,7 @@ export const BITCOIN_ENDPOINTS = {
|
|
|
35
35
|
Sandbox: "https://mempool.space/testnet/api",
|
|
36
36
|
Production: "https://mempool.space/api",
|
|
37
37
|
};
|
|
38
|
+
export const ALEO_NETWORK_CLIENT_URL = "https://api.provable.com/v2";
|
|
38
39
|
export const BOBA_CHAIN_ID = {
|
|
39
40
|
mainnet: 288,
|
|
40
41
|
testnet: 28882,
|
|
@@ -1,45 +1,59 @@
|
|
|
1
|
+
import { type TransactionOptions } from "@provablehq/aleo-types";
|
|
2
|
+
import { AleoNetworkClient } from "@provablehq/sdk/mainnet.js";
|
|
3
|
+
import { AleoNetworkClient as TestnetAleoNetworkClient } from "@provablehq/sdk/testnet.js";
|
|
1
4
|
import { ZebecCardAPIService } from "../helpers/apiHelpers";
|
|
2
|
-
export interface AleoTransition {
|
|
3
|
-
program: string;
|
|
4
|
-
functionName: string;
|
|
5
|
-
inputs: any[];
|
|
6
|
-
}
|
|
7
|
-
export interface AleoTransaction {
|
|
8
|
-
address: string;
|
|
9
|
-
chainId: string;
|
|
10
|
-
transitions: AleoTransition[];
|
|
11
|
-
fee: number;
|
|
12
|
-
feePrivate: boolean;
|
|
13
|
-
}
|
|
14
5
|
export interface AleoWallet {
|
|
15
6
|
address: string;
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
decrypt: (cipherText: string) => Promise<string>;
|
|
8
|
+
requestRecords: (program: string, includePlaintext?: boolean | undefined) => Promise<unknown[]>;
|
|
9
|
+
executeTransaction: (options: TransactionOptions) => Promise<{
|
|
18
10
|
transactionId: string;
|
|
19
11
|
}>;
|
|
20
12
|
}
|
|
21
|
-
export type
|
|
13
|
+
export type AleoTransferCreditParams = {
|
|
22
14
|
amount: number | string;
|
|
23
|
-
|
|
15
|
+
transferType?: "public" | "private";
|
|
24
16
|
fee?: number;
|
|
25
|
-
|
|
17
|
+
privateFee?: boolean;
|
|
26
18
|
};
|
|
27
|
-
export type
|
|
28
|
-
|
|
29
|
-
tokenDecimals: number;
|
|
30
|
-
tokenSymbol: string;
|
|
31
|
-
chainId: string;
|
|
19
|
+
export type AleoTransferStableCoinParams = {
|
|
20
|
+
programId: "usad_stablecoin.aleo" | "usdcx_stablecoin.aleo";
|
|
32
21
|
amount: number | string;
|
|
22
|
+
transferType?: "public" | "private";
|
|
33
23
|
fee?: number;
|
|
34
|
-
|
|
24
|
+
privateFee?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare const NETWORK_CONFIG: {
|
|
27
|
+
mainnet: {
|
|
28
|
+
explorer: string;
|
|
29
|
+
stablecoins: {
|
|
30
|
+
usad: string;
|
|
31
|
+
usdcx: string;
|
|
32
|
+
};
|
|
33
|
+
freezeListApi: {
|
|
34
|
+
usad: string;
|
|
35
|
+
usdcx: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
testnet: {
|
|
39
|
+
explorer: string;
|
|
40
|
+
stablecoins: {
|
|
41
|
+
usad: string;
|
|
42
|
+
usdcx: string;
|
|
43
|
+
};
|
|
44
|
+
freezeListApi: {
|
|
45
|
+
usad: string;
|
|
46
|
+
usdcx: string;
|
|
47
|
+
};
|
|
48
|
+
};
|
|
35
49
|
};
|
|
36
50
|
export declare class AleoService {
|
|
37
51
|
readonly wallet: AleoWallet;
|
|
38
52
|
readonly sandbox: boolean;
|
|
39
53
|
readonly apiService: ZebecCardAPIService;
|
|
54
|
+
readonly networkClient: AleoNetworkClient | TestnetAleoNetworkClient;
|
|
40
55
|
constructor(wallet: AleoWallet, options?: {
|
|
41
56
|
sandbox?: boolean;
|
|
42
|
-
aleoNetworkClientUrl?: string;
|
|
43
57
|
});
|
|
44
58
|
/**
|
|
45
59
|
* Fetches the Bitcoin vault address.
|
|
@@ -50,15 +64,27 @@ export declare class AleoService {
|
|
|
50
64
|
address: string;
|
|
51
65
|
tag?: string;
|
|
52
66
|
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Fetch unspent records for a program, decrypt them, and return the one
|
|
69
|
+
* with the highest non-zero balance as a single-line plaintext string.
|
|
70
|
+
*/
|
|
71
|
+
private _getRecord;
|
|
72
|
+
/**
|
|
73
|
+
* Build a Sealance Merkle exclusion proof proving the sender is NOT on the
|
|
74
|
+
* program's freeze list. Required for compliant stablecoin transfers.
|
|
75
|
+
*/
|
|
76
|
+
private _getComplianceProof;
|
|
53
77
|
/**
|
|
54
78
|
* Transfer native Aleo credits to the specified recipient.
|
|
55
|
-
*
|
|
56
|
-
* @param recipient - The recipient's Aleo address.
|
|
57
79
|
*/
|
|
58
|
-
|
|
80
|
+
transferCredit(params: AleoTransferCreditParams): Promise<{
|
|
59
81
|
transactionId: string;
|
|
60
82
|
}>;
|
|
61
|
-
|
|
83
|
+
transferStableCoin(params: AleoTransferStableCoinParams): Promise<{
|
|
62
84
|
transactionId: string;
|
|
63
85
|
}>;
|
|
86
|
+
getPublicBalance(): Promise<string>;
|
|
87
|
+
getPublicTokenBalance(tokenProgramId: string, tokenSymbol: string): Promise<string>;
|
|
88
|
+
getPrivateBalance(): Promise<string>;
|
|
89
|
+
getPrivateTokenBalance(tokenProgramId: string, tokenSymbol: string): Promise<string>;
|
|
64
90
|
}
|
|
@@ -1,13 +1,45 @@
|
|
|
1
|
+
import { Network } from "@provablehq/aleo-types";
|
|
2
|
+
import { AleoNetworkClient, SealanceMerkleTree } from "@provablehq/sdk/mainnet.js";
|
|
3
|
+
import { AleoNetworkClient as TestnetAleoNetworkClient, SealanceMerkleTree as TestnetSealanceMerkleTree, } from "@provablehq/sdk/testnet.js";
|
|
4
|
+
import { ALEO_NETWORK_CLIENT_URL } from "../constants";
|
|
1
5
|
import { ZebecCardAPIService } from "../helpers/apiHelpers";
|
|
2
|
-
import {
|
|
6
|
+
import { fromMicroUnits, getTokenBySymbol, toMicroUnits } from "../utils";
|
|
7
|
+
export const NETWORK_CONFIG = {
|
|
8
|
+
[Network.MAINNET]: {
|
|
9
|
+
explorer: "https://explorer.provable.com/transaction",
|
|
10
|
+
stablecoins: {
|
|
11
|
+
usad: "usad_stablecoin.aleo",
|
|
12
|
+
usdcx: "usdcx_stablecoin.aleo",
|
|
13
|
+
},
|
|
14
|
+
freezeListApi: {
|
|
15
|
+
usad: "https://api.explorer.provable.com/v2/mainnet/programs/usad_freezelist.aleo/compliance/freeze-list",
|
|
16
|
+
usdcx: "https://api.explorer.provable.com/v2/mainnet/programs/usdcx_freezelist.aleo/compliance/freeze-list",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
[Network.TESTNET]: {
|
|
20
|
+
explorer: "https://explorer.provable.com/testnet/transaction",
|
|
21
|
+
stablecoins: {
|
|
22
|
+
usad: "test_usad_stablecoin.aleo",
|
|
23
|
+
usdcx: "test_usdcx_stablecoin.aleo",
|
|
24
|
+
},
|
|
25
|
+
freezeListApi: {
|
|
26
|
+
usad: "https://api.explorer.provable.com/v2/testnet/programs/test_usad_freezelist.aleo/compliance/freeze-list",
|
|
27
|
+
usdcx: "https://api.explorer.provable.com/v2/testnet/programs/test_usdcx_freezelist.aleo/compliance/freeze-list",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
3
31
|
export class AleoService {
|
|
4
32
|
wallet;
|
|
5
33
|
sandbox;
|
|
6
34
|
apiService;
|
|
35
|
+
networkClient;
|
|
7
36
|
constructor(wallet, options) {
|
|
8
37
|
this.wallet = wallet;
|
|
9
38
|
this.sandbox = options?.sandbox || false;
|
|
10
39
|
this.apiService = new ZebecCardAPIService(options?.sandbox || false);
|
|
40
|
+
this.networkClient = this.sandbox
|
|
41
|
+
? new TestnetAleoNetworkClient(ALEO_NETWORK_CLIENT_URL)
|
|
42
|
+
: new AleoNetworkClient(ALEO_NETWORK_CLIENT_URL);
|
|
11
43
|
}
|
|
12
44
|
/**
|
|
13
45
|
* Fetches the Bitcoin vault address.
|
|
@@ -18,57 +50,231 @@ export class AleoService {
|
|
|
18
50
|
const data = await this.apiService.fetchVault(symbol);
|
|
19
51
|
return data;
|
|
20
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Fetch unspent records for a program, decrypt them, and return the one
|
|
55
|
+
* with the highest non-zero balance as a single-line plaintext string.
|
|
56
|
+
*/
|
|
57
|
+
async _getRecord(program, includePlaintext = false) {
|
|
58
|
+
const records = await this.wallet.requestRecords(program, includePlaintext);
|
|
59
|
+
console.debug("Fetched records:", records);
|
|
60
|
+
const unspent = records?.filter((r) => typeof r === "object" && r !== null && "spent" in r && !r.spent);
|
|
61
|
+
if (!unspent?.length) {
|
|
62
|
+
throw new Error(`No unspent ${program} records found`);
|
|
63
|
+
}
|
|
64
|
+
// Decrypt all unspent records in parallel (fast with AutoDecrypt permission)
|
|
65
|
+
const decrypted = await Promise.all(unspent.map(async (rec) => {
|
|
66
|
+
if (typeof rec !== "object" ||
|
|
67
|
+
rec === null ||
|
|
68
|
+
!("recordCiphertext" in rec) ||
|
|
69
|
+
typeof rec.recordCiphertext !== "string") {
|
|
70
|
+
throw new Error("Invalid record format");
|
|
71
|
+
}
|
|
72
|
+
console.debug("Decrypting record:", rec);
|
|
73
|
+
const plaintext = await this.wallet.decrypt(rec.recordCiphertext);
|
|
74
|
+
console.debug("Decrypted plaintext:", plaintext);
|
|
75
|
+
return plaintext.replace(/\s+/g, " ").trim();
|
|
76
|
+
}));
|
|
77
|
+
// Find records with non-zero balance, sorted highest-first
|
|
78
|
+
const withBalance = decrypted
|
|
79
|
+
.map((line) => {
|
|
80
|
+
const match = line.match(/microcredits:\s*(\d+)u64/) || line.match(/amount:\s*(\d+)u\d+/);
|
|
81
|
+
return { line, balance: match ? BigInt(match[1]) : 0n };
|
|
82
|
+
})
|
|
83
|
+
.filter((r) => r.balance > 0n)
|
|
84
|
+
.sort((a, b) => (b.balance > a.balance ? 1 : -1));
|
|
85
|
+
if (!withBalance.length) {
|
|
86
|
+
throw new Error(`No ${program} records with balance found`);
|
|
87
|
+
}
|
|
88
|
+
return withBalance[0].line;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build a Sealance Merkle exclusion proof proving the sender is NOT on the
|
|
92
|
+
* program's freeze list. Required for compliant stablecoin transfers.
|
|
93
|
+
*/
|
|
94
|
+
async _getComplianceProof(stablecoinKey, senderAddress, network) {
|
|
95
|
+
if (network === Network.CANARY) {
|
|
96
|
+
throw new Error("Compliance proof generation is not supported on canary network");
|
|
97
|
+
}
|
|
98
|
+
const sealance = this.sandbox ? new TestnetSealanceMerkleTree() : new SealanceMerkleTree();
|
|
99
|
+
const url = NETWORK_CONFIG[network].freezeListApi[stablecoinKey];
|
|
100
|
+
const res = await fetch(url);
|
|
101
|
+
const freezeList = await res.json();
|
|
102
|
+
const tree = sealance.convertTreeToBigInt(freezeList);
|
|
103
|
+
const [leftIdx, rightIdx] = sealance.getLeafIndices(tree, senderAddress);
|
|
104
|
+
const leftProof = sealance.getSiblingPath(tree, leftIdx, 16);
|
|
105
|
+
const rightProof = sealance.getSiblingPath(tree, rightIdx, 16);
|
|
106
|
+
return sealance.formatMerkleProof([leftProof, rightProof]);
|
|
107
|
+
}
|
|
21
108
|
/**
|
|
22
109
|
* Transfer native Aleo credits to the specified recipient.
|
|
23
|
-
*
|
|
24
|
-
* @param recipient - The recipient's Aleo address.
|
|
25
110
|
*/
|
|
26
|
-
async
|
|
27
|
-
const { amount
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const functionName = "transfer_public";
|
|
111
|
+
async transferCredit(params) {
|
|
112
|
+
const { amount } = params;
|
|
113
|
+
const transferType = params.transferType || "public";
|
|
114
|
+
const privateFee = params?.privateFee || false;
|
|
115
|
+
const fee = toMicroUnits(params.fee || 0.1, 6);
|
|
32
116
|
const vault = await this.fetchVault("ALEO");
|
|
33
117
|
const recipient = vault.address;
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
118
|
+
console.log("recipient:", recipient);
|
|
119
|
+
const amountInMicroCredits = toMicroUnits(amount, 6, "u64");
|
|
120
|
+
const PROGRAM_NAME = "credits.aleo";
|
|
121
|
+
const functionName = transferType === "public" ? "transfer_public" : "transfer_private";
|
|
122
|
+
let inputs;
|
|
123
|
+
switch (functionName) {
|
|
124
|
+
case "transfer_public":
|
|
125
|
+
inputs = [recipient, `${amountInMicroCredits}u64`];
|
|
126
|
+
break;
|
|
127
|
+
case "transfer_private": {
|
|
128
|
+
const record = await this._getRecord(PROGRAM_NAME);
|
|
129
|
+
inputs = [record, recipient, amountInMicroCredits];
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
default:
|
|
133
|
+
throw new Error("Invalid or Unsupported transfer type");
|
|
134
|
+
}
|
|
135
|
+
if (functionName !== "transfer_public") {
|
|
136
|
+
throw new Error("Only public transfers are currently supported for credits. Private and cross-type transfers require additional implementation.");
|
|
137
|
+
}
|
|
138
|
+
const result = await this.wallet.executeTransaction({
|
|
139
|
+
program: PROGRAM_NAME,
|
|
140
|
+
function: functionName,
|
|
141
|
+
inputs,
|
|
142
|
+
fee: Number(fee),
|
|
143
|
+
privateFee,
|
|
144
|
+
});
|
|
48
145
|
return result;
|
|
49
146
|
}
|
|
50
|
-
async
|
|
51
|
-
const {
|
|
52
|
-
const
|
|
53
|
-
const
|
|
147
|
+
async transferStableCoin(params) {
|
|
148
|
+
const { amount } = params;
|
|
149
|
+
const transferType = params.transferType || "public";
|
|
150
|
+
const privateFee = params?.privateFee || false;
|
|
151
|
+
const fee = toMicroUnits(params.fee || 0.1, 6);
|
|
152
|
+
const programId = this.sandbox ? `test_${params.programId}` : params.programId;
|
|
153
|
+
const tokenSymbol = params.programId === "usad_stablecoin.aleo" ? "USAD" : "USDCX";
|
|
154
|
+
const functionName = transferType === "public" ? "transfer_public" : "transfer_private";
|
|
54
155
|
const vault = await this.fetchVault(tokenSymbol);
|
|
55
156
|
const recipient = vault.address;
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
157
|
+
const amountInMicroUnits = toMicroUnits(amount, 6, "u128");
|
|
158
|
+
let inputs;
|
|
159
|
+
switch (functionName) {
|
|
160
|
+
case "transfer_public":
|
|
161
|
+
inputs = [recipient, amountInMicroUnits];
|
|
162
|
+
break;
|
|
163
|
+
case "transfer_private": {
|
|
164
|
+
// For private transfer, we need to find a record with sufficient balance
|
|
165
|
+
const [record, complianceProof] = await Promise.all([
|
|
166
|
+
this._getRecord(programId),
|
|
167
|
+
this._getComplianceProof(tokenSymbol.toLowerCase(), this.wallet.address, this.sandbox ? Network.TESTNET : Network.MAINNET),
|
|
168
|
+
]);
|
|
169
|
+
inputs = [recipient, amountInMicroUnits, record, complianceProof];
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
throw new Error("Invalid or Unsupported transfer type");
|
|
174
|
+
}
|
|
175
|
+
const result = await this.wallet.executeTransaction({
|
|
176
|
+
fee: Number(fee),
|
|
177
|
+
privateFee,
|
|
60
178
|
program: programId,
|
|
61
|
-
functionName,
|
|
62
|
-
inputs
|
|
63
|
-
};
|
|
64
|
-
const transaction = {
|
|
65
|
-
address: this.wallet.address,
|
|
66
|
-
chainId,
|
|
67
|
-
transitions: [transition],
|
|
68
|
-
fee,
|
|
69
|
-
feePrivate,
|
|
70
|
-
};
|
|
71
|
-
const result = await this.wallet.requestTransaction(transaction);
|
|
179
|
+
function: functionName,
|
|
180
|
+
inputs,
|
|
181
|
+
});
|
|
72
182
|
return result;
|
|
73
183
|
}
|
|
184
|
+
async getPublicBalance() {
|
|
185
|
+
const balance = await this.networkClient.getPublicBalance(this.wallet.address);
|
|
186
|
+
const formattedAmount = fromMicroUnits(balance);
|
|
187
|
+
return formattedAmount;
|
|
188
|
+
}
|
|
189
|
+
async getPublicTokenBalance(tokenProgramId, tokenSymbol) {
|
|
190
|
+
const tokenMetadata = await getTokenBySymbol(tokenSymbol, this.sandbox ? "testnet" : "mainnet");
|
|
191
|
+
if (!("decimals" in tokenMetadata)) {
|
|
192
|
+
throw new Error(`Token metadata for ${tokenSymbol} does not include decimals.`);
|
|
193
|
+
}
|
|
194
|
+
const mappingNames = await this.networkClient.getProgramMappingNames(tokenProgramId);
|
|
195
|
+
const balanceMappingName = mappingNames.includes("balances")
|
|
196
|
+
? "balances"
|
|
197
|
+
: mappingNames.includes("account")
|
|
198
|
+
? "account"
|
|
199
|
+
: null;
|
|
200
|
+
if (!balanceMappingName) {
|
|
201
|
+
throw new Error("No public balance mapping found (no 'balances' or 'account').");
|
|
202
|
+
}
|
|
203
|
+
const balance = await this.networkClient.getProgramMappingValue(tokenProgramId, balanceMappingName, this.wallet.address);
|
|
204
|
+
if (balance) {
|
|
205
|
+
const regex = /(\d+)u\d+/;
|
|
206
|
+
const match = balance.match(regex);
|
|
207
|
+
if (match) {
|
|
208
|
+
const amount = match[1];
|
|
209
|
+
const formattedAmount = fromMicroUnits(amount, tokenMetadata.decimals);
|
|
210
|
+
return formattedAmount;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
throw new Error(`Invalid balance format: ${balance}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
return "0";
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async getPrivateBalance() {
|
|
221
|
+
const programId = "credits.aleo";
|
|
222
|
+
const records = await this.wallet.requestRecords(programId, false);
|
|
223
|
+
if (!records) {
|
|
224
|
+
throw new Error(`No records found for program ${programId}`);
|
|
225
|
+
}
|
|
226
|
+
// console.log("Fetched Records:", records);
|
|
227
|
+
const unspent = records.filter((r) => r && typeof r === "object" && "spent" in r && !r.spent);
|
|
228
|
+
if (!unspent || !unspent.length) {
|
|
229
|
+
throw new Error(`No unspent ${programId} records found`);
|
|
230
|
+
}
|
|
231
|
+
const decrypted = await Promise.all(unspent.map(async (rec) => {
|
|
232
|
+
if (!rec ||
|
|
233
|
+
typeof rec !== "object" ||
|
|
234
|
+
!("recordCiphertext" in rec) ||
|
|
235
|
+
typeof rec.recordCiphertext !== "string") {
|
|
236
|
+
throw new Error(`Invalid record format: ${JSON.stringify(rec)}`);
|
|
237
|
+
}
|
|
238
|
+
const plaintext = await this.wallet.decrypt(rec.recordCiphertext);
|
|
239
|
+
return plaintext.replace(/\s+/g, " ").trim();
|
|
240
|
+
}));
|
|
241
|
+
const balance = decrypted
|
|
242
|
+
.map((line) => {
|
|
243
|
+
const match = line.match(/microcredits:\s*(\d+)u64/);
|
|
244
|
+
return match ? BigInt(match[1]) : 0n;
|
|
245
|
+
})
|
|
246
|
+
.reduce((acc, val) => acc + val, 0n);
|
|
247
|
+
return fromMicroUnits(balance, 6);
|
|
248
|
+
}
|
|
249
|
+
async getPrivateTokenBalance(tokenProgramId, tokenSymbol) {
|
|
250
|
+
const records = await this.wallet.requestRecords(tokenProgramId, false);
|
|
251
|
+
if (!records) {
|
|
252
|
+
throw new Error(`No records found for program ${tokenProgramId}`);
|
|
253
|
+
}
|
|
254
|
+
const unspent = records.filter((r) => r && typeof r === "object" && "spent" in r && !r.spent);
|
|
255
|
+
if (!unspent || !unspent.length) {
|
|
256
|
+
throw new Error(`No unspent ${tokenProgramId} records found`);
|
|
257
|
+
}
|
|
258
|
+
const decrypted = await Promise.all(unspent.map(async (rec) => {
|
|
259
|
+
if (!rec ||
|
|
260
|
+
typeof rec !== "object" ||
|
|
261
|
+
!("recordCiphertext" in rec) ||
|
|
262
|
+
typeof rec.recordCiphertext !== "string") {
|
|
263
|
+
throw new Error(`Invalid record format: ${JSON.stringify(rec)}`);
|
|
264
|
+
}
|
|
265
|
+
const plaintext = await this.wallet.decrypt(rec.recordCiphertext);
|
|
266
|
+
return plaintext.replace(/\s+/g, " ").trim();
|
|
267
|
+
}));
|
|
268
|
+
const balance = decrypted
|
|
269
|
+
.map((line) => {
|
|
270
|
+
const match = line.match(/amount:\s*(\d+)u\d+/);
|
|
271
|
+
return match ? BigInt(match[1]) : 0n;
|
|
272
|
+
})
|
|
273
|
+
.reduce((acc, val) => acc + val, 0n);
|
|
274
|
+
const tokenMetadata = await getTokenBySymbol(tokenSymbol, this.sandbox ? "testnet" : "mainnet");
|
|
275
|
+
if (!("decimals" in tokenMetadata)) {
|
|
276
|
+
throw new Error(`Token metadata for ${tokenSymbol} does not include decimals.`);
|
|
277
|
+
}
|
|
278
|
+
return fromMicroUnits(balance, tokenMetadata.decimals);
|
|
279
|
+
}
|
|
74
280
|
}
|
|
File without changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// import {
|
|
3
|
+
// type AuthTokenProvider,
|
|
4
|
+
// LedgerController,
|
|
5
|
+
// localNetStaticConfig,
|
|
6
|
+
// TokenStandardController,
|
|
7
|
+
// ValidatorController,
|
|
8
|
+
// type WalletSDK,
|
|
9
|
+
// WalletSDKImpl,
|
|
10
|
+
// } from "@canton-network/wallet-sdk";
|
|
11
|
+
// import { ZebecCardAPIService } from "../helpers/apiHelpers";
|
|
12
|
+
// export interface CantonWallet {
|
|
13
|
+
// partyId: string;
|
|
14
|
+
// executeTransaction: (command: unknown, disclosedContractId: unknown[]) => Promise<string>;
|
|
15
|
+
// }
|
|
16
|
+
// export interface CantonConfig {
|
|
17
|
+
// ledgerApiUrl: string;
|
|
18
|
+
// validatorAppApiUrl: string;
|
|
19
|
+
// }
|
|
20
|
+
// export function createLedgerFactory(ledgerApiUrl: string) {
|
|
21
|
+
// return (userId: string, authTokenProvider: AuthTokenProvider, isAdmin: boolean) => {
|
|
22
|
+
// return new LedgerController(
|
|
23
|
+
// userId,
|
|
24
|
+
// new URL(ledgerApiUrl),
|
|
25
|
+
// undefined,
|
|
26
|
+
// isAdmin,
|
|
27
|
+
// authTokenProvider,
|
|
28
|
+
// );
|
|
29
|
+
// };
|
|
30
|
+
// }
|
|
31
|
+
// export function createValidatorFactory(validatorAppApiUrl: string) {
|
|
32
|
+
// return (userId: string, authTokenProvider: AuthTokenProvider) => {
|
|
33
|
+
// return new ValidatorController(userId, new URL(validatorAppApiUrl), authTokenProvider);
|
|
34
|
+
// };
|
|
35
|
+
// }
|
|
36
|
+
// export function createTokenStandardFactory(ledgerApiUrl: string, validatorAppApiUrl: string) {
|
|
37
|
+
// return (userId: string, authTokenProvider: AuthTokenProvider, isAdmin: boolean) => {
|
|
38
|
+
// return new TokenStandardController(
|
|
39
|
+
// userId,
|
|
40
|
+
// new URL(ledgerApiUrl),
|
|
41
|
+
// new URL(validatorAppApiUrl),
|
|
42
|
+
// undefined,
|
|
43
|
+
// authTokenProvider,
|
|
44
|
+
// isAdmin,
|
|
45
|
+
// );
|
|
46
|
+
// };
|
|
47
|
+
// }
|
|
48
|
+
// export class CantonService {
|
|
49
|
+
// readonly cantonWalletSdk: WalletSDK;
|
|
50
|
+
// private apiService: ZebecCardAPIService;
|
|
51
|
+
// constructor(
|
|
52
|
+
// readonly wallet: CantonWallet,
|
|
53
|
+
// readonly cantonConfig: CantonConfig,
|
|
54
|
+
// sdkOptions?: { sandbox?: boolean },
|
|
55
|
+
// ) {
|
|
56
|
+
// this.apiService = new ZebecCardAPIService(sdkOptions?.sandbox || false);
|
|
57
|
+
// this.cantonWalletSdk = new WalletSDKImpl().configure({
|
|
58
|
+
// logger: console,
|
|
59
|
+
// ledgerFactory: createLedgerFactory(cantonConfig.ledgerApiUrl),
|
|
60
|
+
// validatorFactory: createValidatorFactory(cantonConfig.validatorAppApiUrl),
|
|
61
|
+
// tokenStandardFactory: createTokenStandardFactory(
|
|
62
|
+
// cantonConfig.ledgerApiUrl,
|
|
63
|
+
// cantonConfig.validatorAppApiUrl,
|
|
64
|
+
// ),
|
|
65
|
+
// });
|
|
66
|
+
// }
|
|
67
|
+
// async connect() {
|
|
68
|
+
// await this.cantonWalletSdk.connect();
|
|
69
|
+
// }
|
|
70
|
+
// async fetchVault(symbol = "CANTON"): Promise<{ address: string; tag?: string }> {
|
|
71
|
+
// const data = await this.apiService.fetchVault(symbol);
|
|
72
|
+
// return data;
|
|
73
|
+
// }
|
|
74
|
+
// // methods for transfering natve assets
|
|
75
|
+
// async transferNative(params: {
|
|
76
|
+
// amount: number | string;
|
|
77
|
+
// instrumentId: string;
|
|
78
|
+
// instrumentAdmin: string;
|
|
79
|
+
// }) {
|
|
80
|
+
// const vault = await this.fetchVault("CANTON");
|
|
81
|
+
// const sender = this.wallet.partyId;
|
|
82
|
+
// const receiver = vault.address;
|
|
83
|
+
// const memo = vault.tag;
|
|
84
|
+
// // implement transfer logic here
|
|
85
|
+
// this.cantonWalletSdk.tokenStandard?.setTransferFactoryRegistryUrl(
|
|
86
|
+
// localNetStaticConfig.LOCALNET_REGISTRY_API_URL,
|
|
87
|
+
// );
|
|
88
|
+
// const [transferCommand, disclosedContracts] =
|
|
89
|
+
// // biome-ignore lint/style/noNonNullAssertion: we can be sure that tokenStandard is defined here since we set its factory in the SDK constructor
|
|
90
|
+
// await this.cantonWalletSdk.tokenStandard!.createTransfer(
|
|
91
|
+
// sender,
|
|
92
|
+
// receiver,
|
|
93
|
+
// params.amount.toString(),
|
|
94
|
+
// {
|
|
95
|
+
// instrumentId: params.instrumentId,
|
|
96
|
+
// instrumentAdmin: params.instrumentAdmin,
|
|
97
|
+
// },
|
|
98
|
+
// [],
|
|
99
|
+
// memo,
|
|
100
|
+
// );
|
|
101
|
+
// return this.wallet.executeTransaction(transferCommand, disclosedContracts);
|
|
102
|
+
// }
|
|
103
|
+
// }
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./xrplService";
|
|
3
|
-
export * from "./nearService";
|
|
1
|
+
export * from "./aleoService";
|
|
4
2
|
export * from "./algorandService";
|
|
5
|
-
export * from "./xdbService";
|
|
6
|
-
export * from "./octaService";
|
|
7
3
|
export * from "./bobaService";
|
|
4
|
+
export * from "./nearService";
|
|
5
|
+
export * from "./octaService";
|
|
8
6
|
export * from "./quaiService";
|
|
7
|
+
export * from "./stellarService";
|
|
8
|
+
export * from "./xdbService";
|
|
9
|
+
export * from "./xrplService";
|
package/dist/services/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export * from "./
|
|
2
|
-
export * from "./xrplService";
|
|
3
|
-
export * from "./nearService";
|
|
1
|
+
export * from "./aleoService";
|
|
4
2
|
export * from "./algorandService";
|
|
5
|
-
export * from "./xdbService";
|
|
6
|
-
export * from "./octaService";
|
|
7
3
|
export * from "./bobaService";
|
|
4
|
+
// export * from "./cantonService";
|
|
5
|
+
export * from "./nearService";
|
|
6
|
+
export * from "./octaService";
|
|
8
7
|
export * from "./quaiService";
|
|
8
|
+
export * from "./stellarService";
|
|
9
|
+
export * from "./xdbService";
|
|
10
|
+
export * from "./xrplService";
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import algosdk from "algosdk";
|
|
1
|
+
import type algosdk from "algosdk";
|
|
2
|
+
import { BigNumber } from "bignumber.js";
|
|
2
3
|
/**
|
|
3
4
|
* Convert ALGO to microAlgos
|
|
4
5
|
* @param algos Amount in ALGO
|
|
@@ -34,15 +35,27 @@ export declare function formatAlgorandAsset(microAmount: number | bigint, decima
|
|
|
34
35
|
export declare function getAssetDecimals(client: algosdk.Algodv2, assetId: number): Promise<number>;
|
|
35
36
|
/**
|
|
36
37
|
* Convert credits to microcredits
|
|
37
|
-
* @param {number} credits - Amount in credits
|
|
38
|
-
* @param {number} decimals - Number of decimals for the token (default is 6)
|
|
39
|
-
* @returns {number} Amount in microcredits
|
|
40
38
|
*/
|
|
41
|
-
export declare function
|
|
39
|
+
export declare function toMicroUnits(credits: BigNumber.Value, decimals?: number, typeSuffix?: string): string;
|
|
42
40
|
/**
|
|
43
41
|
* Convert microcredits to credits
|
|
44
|
-
* @param {number} microcredits - Amount in microcredits
|
|
45
|
-
* @param {number} decimals - Number of decimals for the token (default is 6)
|
|
46
|
-
* @returns {number} Amount in credits
|
|
47
42
|
*/
|
|
48
|
-
export declare function
|
|
43
|
+
export declare function fromMicroUnits(microcredits: BigNumber.Value, decimals?: number): string;
|
|
44
|
+
export type TokenMetadata = {
|
|
45
|
+
token_id: string;
|
|
46
|
+
token_id_datatype: string | null;
|
|
47
|
+
symbol: string;
|
|
48
|
+
display: string;
|
|
49
|
+
program_name: string;
|
|
50
|
+
decimals: number;
|
|
51
|
+
total_supply: string;
|
|
52
|
+
verified: boolean;
|
|
53
|
+
token_icon_url: string;
|
|
54
|
+
compliance_freeze_list: string;
|
|
55
|
+
price: string;
|
|
56
|
+
price_change_percentage_24h: string;
|
|
57
|
+
fully_diluted_value: string;
|
|
58
|
+
total_market_cap: string;
|
|
59
|
+
volume_24h: string;
|
|
60
|
+
};
|
|
61
|
+
export declare function getTokenBySymbol(tokenSymbol: string, network?: "mainnet" | "testnet"): Promise<TokenMetadata>;
|
package/dist/utils.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BigNumber } from "bignumber.js";
|
|
2
|
+
import { ALEO_NETWORK_CLIENT_URL } from "./constants";
|
|
2
3
|
/**
|
|
3
4
|
* Convert ALGO to microAlgos
|
|
4
5
|
* @param algos Amount in ALGO
|
|
@@ -43,7 +44,13 @@ const ALGORAND_ASSET_DECIMALS_CACHE = new Map();
|
|
|
43
44
|
export async function getAssetDecimals(client, assetId) {
|
|
44
45
|
// Check if we already have this value cached
|
|
45
46
|
if (ALGORAND_ASSET_DECIMALS_CACHE.has(assetId)) {
|
|
46
|
-
|
|
47
|
+
const value = ALGORAND_ASSET_DECIMALS_CACHE.get(assetId);
|
|
48
|
+
if (value) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
throw new Error("Cached value is undefined, this should not happen");
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
55
|
const assetInfo = await client.getAssetByID(assetId).do();
|
|
49
56
|
const decimals = assetInfo.params.decimals;
|
|
@@ -53,19 +60,26 @@ export async function getAssetDecimals(client, assetId) {
|
|
|
53
60
|
}
|
|
54
61
|
/**
|
|
55
62
|
* Convert credits to microcredits
|
|
56
|
-
* @param {number} credits - Amount in credits
|
|
57
|
-
* @param {number} decimals - Number of decimals for the token (default is 6)
|
|
58
|
-
* @returns {number} Amount in microcredits
|
|
59
63
|
*/
|
|
60
|
-
export function
|
|
61
|
-
return BigNumber(credits).times(BigNumber(10).pow(decimals)).toFixed(0)
|
|
64
|
+
export function toMicroUnits(credits, decimals = 6, typeSuffix) {
|
|
65
|
+
return `${BigNumber(credits).times(BigNumber(10).pow(decimals)).toFixed(0)}${typeSuffix || ""}`;
|
|
62
66
|
}
|
|
63
67
|
/**
|
|
64
68
|
* Convert microcredits to credits
|
|
65
|
-
* @param {number} microcredits - Amount in microcredits
|
|
66
|
-
* @param {number} decimals - Number of decimals for the token (default is 6)
|
|
67
|
-
* @returns {number} Amount in credits
|
|
68
69
|
*/
|
|
69
|
-
export function
|
|
70
|
+
export function fromMicroUnits(microcredits, decimals = 6) {
|
|
70
71
|
return BigNumber(microcredits).div(BigNumber(10).pow(decimals)).toFixed();
|
|
71
72
|
}
|
|
73
|
+
export async function getTokenBySymbol(tokenSymbol, network = "mainnet") {
|
|
74
|
+
const response = await fetch(`${ALEO_NETWORK_CLIENT_URL}/${network}/tokens?symbol=${encodeURIComponent(tokenSymbol)}`);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const body = await response.text().catch(() => "");
|
|
77
|
+
throw new Error(`Failed to fetch token decimals for ${tokenSymbol}: ${response.statusText} ${body}`.trim());
|
|
78
|
+
}
|
|
79
|
+
const result = await response.json();
|
|
80
|
+
const tokens = Array.isArray(result) ? result : result?.data;
|
|
81
|
+
if (!Array.isArray(tokens) || tokens.length === 0) {
|
|
82
|
+
throw new Error(`No token found with symbol ${tokenSymbol}`);
|
|
83
|
+
}
|
|
84
|
+
return tokens.find((token) => token.verified) ?? tokens[0];
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
6
6
|
"@near-js/transactions": "^2.5.1",
|
|
7
7
|
"@near-js/types": "^2.5.1",
|
|
8
8
|
"@near-js/utils": "^2.5.1",
|
|
9
|
+
"@provablehq/aleo-types": "^0.3.0-alpha.3",
|
|
10
|
+
"@provablehq/sdk": "^0.9.15",
|
|
9
11
|
"@stellar/stellar-sdk": "^14.4.3",
|
|
10
12
|
"algosdk": "^3.4.0",
|
|
11
13
|
"axios": "^1.11.0",
|
|
12
|
-
"bignumber.js": "^
|
|
13
|
-
"bitcoinjs-lib": "^6.1.7",
|
|
14
|
+
"bignumber.js": "^10.0.2",
|
|
14
15
|
"ethers": "^6.15.0",
|
|
15
16
|
"quais": "^1.0.0-alpha.52",
|
|
16
17
|
"xrpl": "^4.4.1"
|
|
@@ -18,9 +19,11 @@
|
|
|
18
19
|
"description": "An sdk for purchasing silver card in zebec",
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@algorandfoundation/algokit-utils": "^9.1.2",
|
|
22
|
+
"@canton-network/dapp-sdk": "^0.21.2",
|
|
21
23
|
"@near-js/accounts": "^2.5.1",
|
|
22
24
|
"@near-js/keystores": "^2.5.1",
|
|
23
25
|
"@near-js/signers": "^2.5.1",
|
|
26
|
+
"@near-js/tokens": "^2.5.1",
|
|
24
27
|
"@typechain/ethers-v6": "^0.5.1",
|
|
25
28
|
"@types/mocha": "^10.0.10",
|
|
26
29
|
"@types/node": "^24.3.1",
|
|
@@ -50,7 +53,7 @@
|
|
|
50
53
|
},
|
|
51
54
|
"repository": {
|
|
52
55
|
"type": "git",
|
|
53
|
-
"url": "git+
|
|
56
|
+
"url": "git+git@github.com:Zebec-Fintech-Labs/card-sdk-mono.git"
|
|
54
57
|
},
|
|
55
58
|
"scripts": {
|
|
56
59
|
"build": "npm run clean && tsc",
|
|
@@ -59,6 +62,7 @@
|
|
|
59
62
|
"gen:typechain": "typechain --target ethers-v6 --out-dir \"src/artifacts/typechain-types\" \"src/artifacts/abi/*.json\"",
|
|
60
63
|
"test": "ts-mocha -p ./tsconfig.test.json -t 1000000"
|
|
61
64
|
},
|
|
65
|
+
"type": "module",
|
|
62
66
|
"types": "dist/index.d.ts",
|
|
63
|
-
"version": "1.9.0-dev.
|
|
67
|
+
"version": "1.9.0-dev.10"
|
|
64
68
|
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import * as bitcoin from "bitcoinjs-lib";
|
|
2
|
-
interface BitcoinWallet {
|
|
3
|
-
address: string;
|
|
4
|
-
signTransaction: (psbt: bitcoin.Psbt) => Promise<bitcoin.Psbt>;
|
|
5
|
-
broadcastTransaction: (tx: string) => Promise<string>;
|
|
6
|
-
}
|
|
7
|
-
export declare class BitcoinService {
|
|
8
|
-
readonly wallet: BitcoinWallet;
|
|
9
|
-
private apiService;
|
|
10
|
-
private network;
|
|
11
|
-
private apiEndpoint;
|
|
12
|
-
constructor(wallet: BitcoinWallet, sdkOptions?: {
|
|
13
|
-
sandbox?: boolean;
|
|
14
|
-
apiKey?: string;
|
|
15
|
-
});
|
|
16
|
-
/**
|
|
17
|
-
* Fetches the Bitcoin vault address.
|
|
18
|
-
*
|
|
19
|
-
* @returns {Promise<{ address: string }>} A promise that resolves to the vault address.
|
|
20
|
-
*/
|
|
21
|
-
fetchVault(): Promise<{
|
|
22
|
-
address: string;
|
|
23
|
-
tag?: string;
|
|
24
|
-
}>;
|
|
25
|
-
getUTXOs(): Promise<Array<{
|
|
26
|
-
txid: string;
|
|
27
|
-
vout: number;
|
|
28
|
-
value: number;
|
|
29
|
-
rawTx: Buffer;
|
|
30
|
-
}>>;
|
|
31
|
-
private getBalance;
|
|
32
|
-
/**
|
|
33
|
-
* Transfers Bitcoin to the vault address.
|
|
34
|
-
*
|
|
35
|
-
* @param {string} amount - The amount of BTC to transfer in BTC units
|
|
36
|
-
* @param {number} feeRate - Fee rate in satoshis per byte
|
|
37
|
-
* @returns {Promise<string>} - A promise that resolves to the transaction hash
|
|
38
|
-
* @throws {Error} If there is not enough balance or if the transaction fails.
|
|
39
|
-
*/
|
|
40
|
-
transferBTC(amount: string, feeRate?: number): Promise<string>;
|
|
41
|
-
/**
|
|
42
|
-
* Gets the balance of the Bitcoin wallet.
|
|
43
|
-
*
|
|
44
|
-
* @returns {Promise<string>} - A promise that resolves to the wallet balance in BTC
|
|
45
|
-
*/
|
|
46
|
-
getWalletBalance(): Promise<string>;
|
|
47
|
-
}
|
|
48
|
-
export {};
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import axios from "axios";
|
|
2
|
-
import * as bitcoin from "bitcoinjs-lib";
|
|
3
|
-
import { BITCOIN_ENDPOINTS } from "../constants";
|
|
4
|
-
import { ZebecCardAPIService } from "../helpers/apiHelpers";
|
|
5
|
-
export class BitcoinService {
|
|
6
|
-
wallet;
|
|
7
|
-
apiService;
|
|
8
|
-
network;
|
|
9
|
-
apiEndpoint;
|
|
10
|
-
constructor(wallet, sdkOptions) {
|
|
11
|
-
this.wallet = wallet;
|
|
12
|
-
const sandbox = sdkOptions?.sandbox ?? false;
|
|
13
|
-
this.apiService = new ZebecCardAPIService(sandbox);
|
|
14
|
-
this.network = sandbox ? bitcoin.networks.testnet : bitcoin.networks.bitcoin;
|
|
15
|
-
this.apiEndpoint = sandbox ? BITCOIN_ENDPOINTS.Sandbox : BITCOIN_ENDPOINTS.Production;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Fetches the Bitcoin vault address.
|
|
19
|
-
*
|
|
20
|
-
* @returns {Promise<{ address: string }>} A promise that resolves to the vault address.
|
|
21
|
-
*/
|
|
22
|
-
async fetchVault() {
|
|
23
|
-
const data = await this.apiService.fetchVault("BTC");
|
|
24
|
-
return data;
|
|
25
|
-
}
|
|
26
|
-
async getUTXOs() {
|
|
27
|
-
const response = await axios.get(`${this.apiEndpoint}/address/${this.wallet.address}/utxo`);
|
|
28
|
-
console.log("utxos:", response.data);
|
|
29
|
-
return Promise.all(response.data.map(async (utxo) => {
|
|
30
|
-
const rawTx = await axios.get(`${this.apiEndpoint}/tx/${utxo.txid}/hex`);
|
|
31
|
-
console.log("txHex:", rawTx.data);
|
|
32
|
-
if (!rawTx.data) {
|
|
33
|
-
throw new Error("Transaction not found");
|
|
34
|
-
}
|
|
35
|
-
// const scriptPubKey = txResponse.data.vout[utxo.vout].scriptpubkey;
|
|
36
|
-
return {
|
|
37
|
-
txid: utxo.txid,
|
|
38
|
-
vout: utxo.vout,
|
|
39
|
-
value: utxo.value,
|
|
40
|
-
rawTx: Buffer.from(rawTx.data, "hex"),
|
|
41
|
-
};
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
44
|
-
async getBalance() {
|
|
45
|
-
const response = await axios.get(`${this.apiEndpoint}/address/${this.wallet.address}/utxo`);
|
|
46
|
-
const utxos = response.data;
|
|
47
|
-
return utxos.reduce((sum, utxo) => sum + utxo.value, 0);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Transfers Bitcoin to the vault address.
|
|
51
|
-
*
|
|
52
|
-
* @param {string} amount - The amount of BTC to transfer in BTC units
|
|
53
|
-
* @param {number} feeRate - Fee rate in satoshis per byte
|
|
54
|
-
* @returns {Promise<string>} - A promise that resolves to the transaction hash
|
|
55
|
-
* @throws {Error} If there is not enough balance or if the transaction fails.
|
|
56
|
-
*/
|
|
57
|
-
async transferBTC(amount, feeRate = 10) {
|
|
58
|
-
// Convert BTC to satoshis
|
|
59
|
-
const satoshisToSend = Math.floor(Number(amount) * 100_000_000);
|
|
60
|
-
// Fetch deposit address
|
|
61
|
-
const vault = await this.fetchVault();
|
|
62
|
-
console.log({ vault });
|
|
63
|
-
let retries = 0;
|
|
64
|
-
const maxRetries = 5;
|
|
65
|
-
let delay = 1000;
|
|
66
|
-
const psbt = new bitcoin.Psbt({ network: this.network });
|
|
67
|
-
const utxos = await this.getUTXOs();
|
|
68
|
-
let inputAmount = 0;
|
|
69
|
-
for (const utxo of utxos) {
|
|
70
|
-
const transaction = bitcoin.Transaction.fromBuffer(utxo.rawTx);
|
|
71
|
-
const script = transaction.outs[utxo.vout].script;
|
|
72
|
-
const value = transaction.outs[utxo.vout].value;
|
|
73
|
-
inputAmount += value;
|
|
74
|
-
psbt.addInput({
|
|
75
|
-
hash: utxo.txid,
|
|
76
|
-
index: utxo.vout,
|
|
77
|
-
// nonWitnessUtxo: utxo.rawTx,
|
|
78
|
-
witnessUtxo: {
|
|
79
|
-
script: script,
|
|
80
|
-
value: value,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
if (inputAmount >= satoshisToSend)
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
if (inputAmount < satoshisToSend) {
|
|
87
|
-
throw new Error("Insufficient UTXO amount");
|
|
88
|
-
}
|
|
89
|
-
// Add output for payment
|
|
90
|
-
psbt.addOutput({
|
|
91
|
-
// address: vault.address,
|
|
92
|
-
address: "tb1q6h8w53xzj74n28kg8qq3d78xxgrch8zd2km97d",
|
|
93
|
-
value: satoshisToSend,
|
|
94
|
-
});
|
|
95
|
-
// Add change output if necessary
|
|
96
|
-
const estimatedFee = Math.ceil(psbt.toBuffer().length * feeRate);
|
|
97
|
-
// Check wallet balance
|
|
98
|
-
const balance = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
|
|
99
|
-
if (balance < satoshisToSend + estimatedFee) {
|
|
100
|
-
throw new Error("Insufficient balance");
|
|
101
|
-
}
|
|
102
|
-
const changeAmount = inputAmount - satoshisToSend - estimatedFee;
|
|
103
|
-
if (changeAmount > 0) {
|
|
104
|
-
psbt.addOutput({
|
|
105
|
-
address: this.wallet.address,
|
|
106
|
-
value: changeAmount,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
console.log("psbt:", JSON.stringify(psbt));
|
|
110
|
-
// Sign transaction
|
|
111
|
-
const signedPsbt = await this.wallet.signTransaction(psbt);
|
|
112
|
-
const tx = signedPsbt.finalizeAllInputs().extractTransaction();
|
|
113
|
-
while (retries < maxRetries) {
|
|
114
|
-
try {
|
|
115
|
-
// Broadcast transaction
|
|
116
|
-
return this.wallet.broadcastTransaction(tx.toHex());
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
console.debug("error: ", error);
|
|
120
|
-
if (retries >= maxRetries) {
|
|
121
|
-
throw error;
|
|
122
|
-
}
|
|
123
|
-
retries += 1;
|
|
124
|
-
console.debug(`Retrying in ${delay / 1000} seconds...`);
|
|
125
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
126
|
-
delay *= 2; // Exponential backoff
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
throw new Error("Max retries reached");
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Gets the balance of the Bitcoin wallet.
|
|
133
|
-
*
|
|
134
|
-
* @returns {Promise<string>} - A promise that resolves to the wallet balance in BTC
|
|
135
|
-
*/
|
|
136
|
-
async getWalletBalance() {
|
|
137
|
-
try {
|
|
138
|
-
const balanceSats = await this.getBalance();
|
|
139
|
-
return (balanceSats / 100_000_000).toString();
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
console.debug("Error fetching BTC balance:", error);
|
|
143
|
-
return "0";
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|