|
| 1 | +import { |
| 2 | + Account, |
| 3 | + initThreadPool, |
| 4 | + ProgramManager, |
| 5 | + AleoKeyProvider, |
| 6 | + AleoKeyProviderParams, |
| 7 | + TransactionObject, |
| 8 | +} from "@provablehq/sdk/testnet.js"; |
| 9 | +import { CREDITS_PROGRAM_KEYS } from "@provablehq/sdk/testnet.js"; |
| 10 | + |
| 11 | +// Initialize the threadpool to speed up proving. |
| 12 | +await initThreadPool(); |
| 13 | + |
| 14 | +/** |
| 15 | + * A class that wraps the credits.aleo program functionality. |
| 16 | + * |
| 17 | + * Provides methods for all credits.aleo transfer functions: |
| 18 | + * - transferPublic: Public to public transfer |
| 19 | + * - transferPublicToPrivate: Public to private transfer |
| 20 | + * - transferPrivate: Private to private transfer |
| 21 | + * - transferPrivateToPublic: Private to public transfer |
| 22 | + * - join: Combine two private records |
| 23 | + * - split: Split a private record into two |
| 24 | + * |
| 25 | + * @example |
| 26 | + * const account = Account.fromCiphertext(ciphertext, password); |
| 27 | + * const credits = new Credits(account); |
| 28 | + * |
| 29 | + * await credits.transferPublic(recipient, 50000); |
| 30 | + * await credits.transferPrivate(record, recipient, 100000); |
| 31 | + */ |
| 32 | +class Credits { |
| 33 | + private programManager: ProgramManager; |
| 34 | + private keyProvider: AleoKeyProvider; |
| 35 | + private creditsProgram: string; |
| 36 | + private _account: Account; |
| 37 | + |
| 38 | + /** |
| 39 | + * Create a new Credits instance. |
| 40 | + * |
| 41 | + * @param account - The Aleo account to use for transactions |
| 42 | + * @param apiUrl - The API endpoint (defaults to https://api.provable.com/v2) |
| 43 | + */ |
| 44 | + constructor(account: Account, apiUrl: string = "https://api.provable.com/v2") { |
| 45 | + this._account = account; |
| 46 | + this.programManager = new ProgramManager(apiUrl); |
| 47 | + this.programManager.setAccount(account); |
| 48 | + |
| 49 | + this.keyProvider = new AleoKeyProvider(); |
| 50 | + this.keyProvider.useCache(true); |
| 51 | + this.programManager.setKeyProvider(this.keyProvider); |
| 52 | + |
| 53 | + this.creditsProgram = this.programManager.creditsProgram().toString(); |
| 54 | + } |
| 55 | + |
| 56 | + /** |
| 57 | + * Get the account associated with this Credits instance. |
| 58 | + */ |
| 59 | + get account(): Account { |
| 60 | + return this._account; |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * Execute a credits.aleo function. |
| 65 | + */ |
| 66 | + private async execute(functionName: string, inputs: string[]): Promise<string[]> { |
| 67 | + const start = Date.now(); |
| 68 | + console.log(`Starting ${functionName} execution`); |
| 69 | + |
| 70 | + const keyParams = new AleoKeyProviderParams({ |
| 71 | + cacheKey: CREDITS_PROGRAM_KEYS.getKey(functionName).locator, |
| 72 | + }); |
| 73 | + |
| 74 | + const tx = await this.programManager.buildExecutionTransaction({ |
| 75 | + programName: "credits.aleo", |
| 76 | + functionName, |
| 77 | + inputs, |
| 78 | + priorityFee: 0, |
| 79 | + privateFee: false, |
| 80 | + keySearchParams: keyParams, |
| 81 | + program: this.creditsProgram, |
| 82 | + }); |
| 83 | + |
| 84 | + const summary = tx.summary(true) as TransactionObject; |
| 85 | + const outputs = this.extractOutputs(summary); |
| 86 | + |
| 87 | + console.log("Transaction ID:", tx.id()); |
| 88 | + console.log("Outputs:", outputs); |
| 89 | + console.log(`${functionName} finished in ${Date.now() - start}ms`); |
| 90 | + |
| 91 | + return outputs; |
| 92 | + } |
| 93 | + |
| 94 | + /** |
| 95 | + * Extract outputs from a built transaction using its summary. |
| 96 | + * Gets outputs from the execution transition (excludes fee transition). |
| 97 | + */ |
| 98 | + private extractOutputs(summary: TransactionObject): string[] { |
| 99 | + if (!summary.execution) return []; |
| 100 | + |
| 101 | + const execTransition = summary.execution.transitions.find( |
| 102 | + (t) => t.function !== "fee_public" && t.function !== "fee_private", |
| 103 | + ); |
| 104 | + if (!execTransition?.outputs) return []; |
| 105 | + |
| 106 | + return execTransition.outputs.map((o) => { |
| 107 | + if (o.type === "record" && o.value) { |
| 108 | + return String(o.value); |
| 109 | + } else if (o.type === "future") { |
| 110 | + const args = Array.isArray(o.arguments) |
| 111 | + ? o.arguments.map(String).join(", ") |
| 112 | + : ""; |
| 113 | + return `Future { program: ${o.program}, function: ${o.function}, args: [${args}] }`; |
| 114 | + } |
| 115 | + return o.value ? String(o.value) : ""; |
| 116 | + }); |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Transfer credits from the caller's public balance to a recipient's public balance. |
| 121 | + * |
| 122 | + * @param recipient - The Aleo address to receive the credits |
| 123 | + * @param amount - The amount to transfer in microcredits |
| 124 | + * @returns The outputs from the execution |
| 125 | + * |
| 126 | + * @example |
| 127 | + * await credits.transferPublic("aleo1abc...xyz", 1000000); |
| 128 | + */ |
| 129 | + async transferPublic(recipient: string, amount: number): Promise<string[]> { |
| 130 | + return this.execute("transfer_public", [recipient, `${amount}u64`]); |
| 131 | + } |
| 132 | + |
| 133 | + /** |
| 134 | + * Transfer credits from the caller's public balance to a recipient as a private record. |
| 135 | + * |
| 136 | + * @param recipient - The Aleo address to receive the private record |
| 137 | + * @param amount - The amount to transfer in microcredits |
| 138 | + * @returns The outputs containing the new private record |
| 139 | + * |
| 140 | + * @example |
| 141 | + * await credits.transferPublicToPrivate("aleo1abc...xyz", 500000); |
| 142 | + */ |
| 143 | + async transferPublicToPrivate(recipient: string, amount: number): Promise<string[]> { |
| 144 | + return this.execute("transfer_public_to_private", [recipient, `${amount}u64`]); |
| 145 | + } |
| 146 | + |
| 147 | + /** |
| 148 | + * Transfer credits privately from one record to another address. |
| 149 | + * |
| 150 | + * @param record - The sender's credits record (plaintext format) |
| 151 | + * @param recipient - The Aleo address to receive the credits |
| 152 | + * @param amount - The amount to transfer in microcredits |
| 153 | + * @returns The outputs containing recipient record + change record |
| 154 | + * |
| 155 | + * @example |
| 156 | + * await credits.transferPrivate(myRecord, "aleo1abc...xyz", 100000); |
| 157 | + */ |
| 158 | + async transferPrivate(record: string, recipient: string, amount: number): Promise<string[]> { |
| 159 | + return this.execute("transfer_private", [record, recipient, `${amount}u64`]); |
| 160 | + } |
| 161 | + |
| 162 | + /** |
| 163 | + * Transfer credits from a private record to a recipient's public balance. |
| 164 | + * |
| 165 | + * @param record - The sender's credits record (plaintext format) |
| 166 | + * @param recipient - The Aleo address to receive the public credits |
| 167 | + * @param amount - The amount to transfer in microcredits |
| 168 | + * @returns The outputs containing the change record |
| 169 | + * |
| 170 | + * @example |
| 171 | + * await credits.transferPrivateToPublic(myRecord, "aleo1abc...xyz", 50000); |
| 172 | + */ |
| 173 | + async transferPrivateToPublic(record: string, recipient: string, amount: number): Promise<string[]> { |
| 174 | + return this.execute("transfer_private_to_public", [record, recipient, `${amount}u64`]); |
| 175 | + } |
| 176 | + |
| 177 | + /** |
| 178 | + * Combine two private credit records into a single record. |
| 179 | + * |
| 180 | + * @param record1 - The first credits record to combine |
| 181 | + * @param record2 - The second credits record to combine |
| 182 | + * @returns The outputs containing the combined record |
| 183 | + * |
| 184 | + * @example |
| 185 | + * await credits.join(recordA, recordB); |
| 186 | + */ |
| 187 | + async join(record1: string, record2: string): Promise<string[]> { |
| 188 | + return this.execute("join", [record1, record2]); |
| 189 | + } |
| 190 | + |
| 191 | + /** |
| 192 | + * Split a private credit record into two separate records. |
| 193 | + * |
| 194 | + * @param record - The credits record to split |
| 195 | + * @param amount - The amount for the first output record |
| 196 | + * @returns The outputs containing two records |
| 197 | + * |
| 198 | + * @example |
| 199 | + * await credits.split(myRecord, 200000); |
| 200 | + */ |
| 201 | + async split(record: string, amount: number): Promise<string[]> { |
| 202 | + return this.execute("split", [record, `${amount}u64`]); |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +// ============================================================================ |
| 207 | +// Demo: Using the Credits class |
| 208 | +// ============================================================================ |
| 209 | + |
| 210 | +// Import the account |
| 211 | +const accountCiphertext = |
| 212 | + "ciphertext1qvq283j7ujnhz59d4rnu772rfmvf94039x9ekhk2lzuutteqzlghsr3g9824qgw97a79mmdymqdt0ulqdkahq39vnerw2tl7thvvnnunq386jzjnw29e0ghnq7unphgdzw637q3fgvvlkrcywsc5jukkdhss5qq3njp"; |
| 213 | +const account = Account.fromCiphertext(accountCiphertext, "provablealeo1"); |
| 214 | + |
| 215 | +// Create the Credits instance |
| 216 | +const credits = new Credits(account); |
| 217 | + |
| 218 | +// Specify the recipient |
| 219 | +const recipient = "aleo1vskzxa2qqgnhznxsqh6tgq93c30sfkj6xqwe7sr85lgjkexjlcxs3lxhy3"; |
| 220 | + |
| 221 | +// NOTE: These records exist on testnet and are used to build transactions. |
| 222 | +// Since we only build (not broadcast) the transactions, they remain unspent. |
| 223 | + |
| 224 | +const sendRecord = `{ |
| 225 | + owner: aleo1vskzxa2qqgnhznxsqh6tgq93c30sfkj6xqwe7sr85lgjkexjlcxs3lxhy3.private, |
| 226 | + microcredits: 500000u64.private, |
| 227 | + _nonce: 2128807984625485873765840993868794284062894954530194503954279385341936659546group.public, |
| 228 | + _version: 1u8.public |
| 229 | +}`; |
| 230 | + |
| 231 | +const joinRecord = `{ |
| 232 | + owner: aleo1vskzxa2qqgnhznxsqh6tgq93c30sfkj6xqwe7sr85lgjkexjlcxs3lxhy3.private, |
| 233 | + microcredits: 1000000u64.private, |
| 234 | + _nonce: 3679642728562651942188038004588605401119210243204186196628122783406618717891group.public, |
| 235 | + _version: 1u8.public |
| 236 | +}`; |
| 237 | + |
| 238 | +const functions: Record<string, () => Promise<string[]>> = { |
| 239 | + transfer_public: () => credits.transferPublic(recipient, 50000), |
| 240 | + transfer_public_to_private: () => credits.transferPublicToPrivate(recipient, 50000), |
| 241 | + transfer_private: () => credits.transferPrivate(sendRecord, recipient, 100000), |
| 242 | + transfer_private_to_public: () => credits.transferPrivateToPublic(sendRecord, recipient, 50000), |
| 243 | + join: () => credits.join(sendRecord, joinRecord), |
| 244 | + split: () => credits.split(sendRecord, 250000), |
| 245 | +}; |
| 246 | + |
| 247 | +async function main() { |
| 248 | + const selected = process.argv[2]; |
| 249 | + |
| 250 | + if (selected && !functions[selected]) { |
| 251 | + console.error(`Unknown function: ${selected}`); |
| 252 | + console.error(`Available: ${Object.keys(functions).join(", ")}`); |
| 253 | + process.exit(1); |
| 254 | + } |
| 255 | + |
| 256 | + const toRun = selected ? { [selected]: functions[selected] } : functions; |
| 257 | + |
| 258 | + for (const fn of Object.values(toRun)) { |
| 259 | + await fn(); |
| 260 | + } |
| 261 | + |
| 262 | + console.log("\nDone!"); |
| 263 | +} |
| 264 | + |
| 265 | +main().catch(console.error); |
0 commit comments