diff --git a/LICENSE b/LICENSE index 4ce5c30..eef0d9e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2026 MaidToShelly - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 NautilusOSS + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 965669e..b029a22 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,14 @@ Errors: | `arc200_transfer_txn` | Simulated transfer → unsigned txn group | | `arc200_transfer_from_txn` | Simulated `transferFrom` | | `arc200_approve_txn` | Simulated `approve` | +| `nt200_withdraw_txn` | NT200 unwrap (underlying ASA / native) | +| `nt200_deposit_txn` | NT200 wrap (payment or `assetId` + axfer) | +| `nt200_create_balance_box_txn` | NT200 `createBalanceBox` | +| `arc200_exchange` | **XCHG-1:** read `(exchange_asset, sink)` for ASA ↔ ARC-200 exchange | +| `arc200_redeem_txn` | **XCHG-1:** ASA → app + `arc200_redeem` (receive ARC-200 from sink) | +| `arc200_swap_back_txn` | **XCHG-1:** `arc200_transfer` to sink + `arc200_swapBack` (ARC-200 → ASA) | + +**XCHG-1 (optional extension)** — Draft: [ARCFoundry XCHG-1](https://github.com/NautilusOSS/ARCFoundry/blob/main/drafts/XCHG-1.md). Interface id `0xf7bde749`. Use **`arc200_exchange`** first: if it fails, the token does not expose the exchange API. Use **`arc200_transfer_txn`** (and allowances) for ordinary peer-to-peer ARC-200 moves; use **redeem / swap_back** only when moving between the paired ASA and ARC-200 via the app’s sink (e.g. POW/WAD-style positions that wrap an ASA). Redeem requires the user to hold and opt in to the **exchange ASA**; swap back requires ARC-200 balance and usually ASA opt-in to receive the outgoing ASA. ### `arc72_*` (`ulujs` ARC-72) @@ -146,6 +154,7 @@ See [`examples/capabilities.json`](examples/capabilities.json). - **ARC-200 / ARC-72 reads:** Use ulujs `Contract` with the public simulation sender (`oneAddress` from ulujs) for readonly ABI calls. - **Event filters:** `address` is forwarded to the indexer as **`sender`** (arccjs API). - **ARC-72 `setApprovalForAll`:** Implemented against `contractInstance.arc72_setApprovalForAll` because `ulujs` `safe_arc72_setApprovalForAll` references undefined variables (upstream bug). +- **XCHG-1:** `arc200_redeem_txn` uses **arccjs** `setExtraTxns` with an ABI-encoded app call (axfer + redeem in one simulation). `arc200_swap_back_txn` concatenates an ulujs-simulated `arc200_transfer` to `sink` with an `arc200_swapBack` app call, then re-groups with `assignGroupID`. ## License diff --git a/examples/capabilities.json b/examples/capabilities.json index b264e26..b2f8a91 100644 --- a/examples/capabilities.json +++ b/examples/capabilities.json @@ -30,7 +30,10 @@ "arc200_approve_txn", "nt200_withdraw_txn", "nt200_deposit_txn", - "nt200_create_balance_box_txn" + "nt200_create_balance_box_txn", + "arc200_exchange", + "arc200_redeem_txn", + "arc200_swap_back_txn" ], "arc72": [ "arc72_get_metadata", diff --git a/examples/demo.mjs b/examples/demo.mjs index 004427c..bf1dc71 100644 --- a/examples/demo.mjs +++ b/examples/demo.mjs @@ -1,5 +1,5 @@ /** - * Spawns asset-mcp over stdio and exercises ASA, ARC-200, and ARC-72 read tools. + * Spawns asset-mcp over stdio and exercises ASA, ARC-200 (including XCHG-1), and ARC-72 tools. * Run from repo root: node examples/demo.mjs */ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; @@ -9,6 +9,9 @@ import path from "node:path"; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); +// shelly-main wallet (from UluWalletMCP) — used as `sender` for txn demos +const SHELLY_MAIN = "6TLMFPO53BADTZCT5E6OACBGPQMXMOYRLQ62IRCM6IKAYG5V33462TV57E"; + function printToolResult(label, result) { const text = result.content?.find((c) => c.type === "text")?.text; console.log(`\n--- ${label} ---`); @@ -51,6 +54,44 @@ printToolResult( }) ); +// XCHG-1 (optional ARC-200 exchange extension): read + txn builders. +// Typical ARC-200 tokens do not implement `arc200_exchange` — expect MCP error JSON until you use an XCHG-enabled app id. +const XCHG_DEMO_APP = 47138068; // WAD (Whale Asset Dollar) on Voi — illustrates not-implemented vs XCHG-enabled contracts + +printToolResult( + `arc200_exchange (Voi app ${XCHG_DEMO_APP} — XCHG read; fails if extension absent)`, + await client.callTool({ + name: "arc200_exchange", + arguments: { network: "voi-mainnet", appId: XCHG_DEMO_APP }, + }) +); + +printToolResult( + `arc200_redeem_txn (same app — fails if arc200_exchange unavailable)`, + await client.callTool({ + name: "arc200_redeem_txn", + arguments: { + network: "voi-mainnet", + appId: XCHG_DEMO_APP, + sender: SHELLY_MAIN, + amount: "1000000", + }, + }) +); + +printToolResult( + `arc200_swap_back_txn (same app — fails if arc200_exchange unavailable)`, + await client.callTool({ + name: "arc200_swap_back_txn", + arguments: { + network: "voi-mainnet", + appId: XCHG_DEMO_APP, + sender: SHELLY_MAIN, + amount: "1000000", + }, + }) +); + printToolResult( "arc72_total_supply (enVoi .voi collection)", await client.callTool({ @@ -59,9 +100,6 @@ printToolResult( }) ); -// shelly-main wallet (from UluWalletMCP) -const SHELLY_MAIN = "6TLMFPO53BADTZCT5E6OACBGPQMXMOYRLQ62IRCM6IKAYG5V33462TV57E"; - // FINITE (DeFi-nite) on Algorand — ASA 400593267 printToolResult( "asa_get_asset (Algorand FINITE / DeFi-nite)", diff --git a/lib/arc200.js b/lib/arc200.js index 28d2e6c..aac6691 100644 --- a/lib/arc200.js +++ b/lib/arc200.js @@ -6,6 +6,56 @@ import { getClients } from "./clients.js"; import { AssetMcpError, ErrorCodes } from "./errors.js"; import { stripTrailingZeroBytes } from "./strings.js"; +/** + * XCHG-1 (draft) ARC-200 Exchange Extension — optional ASA ↔ ARC-200 flows. + * @see https://github.com/NautilusOSS/ARCFoundry/blob/main/drafts/XCHG-1.md + */ +export const ARC200_XCHG_INTERFACE_ID = "f7bde749"; + +/** @type {import("algosdk").ABIContract} */ +let arc200XchgIface; + +/** + * ARC4 ABI for `arc200_exchange`, `arc200_redeem`, `arc200_swapBack` (read + txn builders). + */ +export const ARC200_XCHG_ABI = { + name: "arc200_xchg", + description: "ARC-200 XCHG-1 exchange extension", + methods: [ + { + name: "arc200_exchange", + args: [], + readonly: true, + returns: { type: "(uint64,address)" }, + desc: "Returns (exchange_asset ASA id, sink address holding ARC-200 for redemption).", + }, + { + name: "arc200_redeem", + args: [{ type: "uint64", name: "amount", desc: "ASA amount (base units) to redeem for ARC-200" }], + readonly: false, + returns: { type: "void" }, + desc: "ASA axfer to app + redeem; ARC-200 sent from sink (grouped atomically).", + }, + { + name: "arc200_swapBack", + args: [ + { type: "uint64", name: "amount", desc: "ARC-200 amount (base units) to swap back to ASA" }, + ], + readonly: false, + returns: { type: "void" }, + desc: "After ARC-200 is sent to sink, swap back to ASA from app holdings.", + }, + ], + events: [], +}; + +function getArc200XchgIface() { + if (!arc200XchgIface) { + arc200XchgIface = new algosdk.ABIContract(ARC200_XCHG_ABI); + } + return arc200XchgIface; +} + /** * @param {string} addr */ @@ -436,3 +486,194 @@ export async function arc200CreateBalanceBoxTxn(networkId, appId, sender, addres Buffer.from(algosdk.encodeUnsignedTransaction(txn)).toString("base64") ); } + +/** + * Decode `arc200_exchange()` return value to plain JSON-friendly fields. + * @param {unknown} rv + */ +function normalizeArc200ExchangeResult(rv) { + /** @type {bigint | number} */ + let exchangeAssetRaw; + /** @type {string} */ + let sink; + if (Array.isArray(rv) && rv.length >= 2) { + exchangeAssetRaw = /** @type {bigint | number} */ (rv[0]); + const s = rv[1]; + sink = typeof s === "string" ? s : algosdk.encodeAddress(s); + } else if (rv != null && typeof rv === "object" && !Array.isArray(rv)) { + const o = /** @type {Record} */ (rv); + exchangeAssetRaw = /** @type {bigint | number} */ ( + o.exchange_asset ?? o[0] ?? o.exchangeAsset + ); + const s = o.sink ?? o[1]; + sink = + typeof s === "string" ? s : s != null ? algosdk.encodeAddress(/** @type {Uint8Array} */ (s)) : ""; + } else { + throw new AssetMcpError( + ErrorCodes.CONTRACT_CALL_FAILED, + "unexpected arc200_exchange return shape" + ); + } + const exchangeAssetBi = BigInt(String(exchangeAssetRaw)); + if (exchangeAssetBi > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new AssetMcpError( + ErrorCodes.CONTRACT_CALL_FAILED, + "exchange_asset ASA id exceeds safe integer range for tooling" + ); + } + const exchangeAsset = Number(exchangeAssetBi); + if (!sink || !algosdk.isValidAddress(sink)) { + throw new AssetMcpError(ErrorCodes.CONTRACT_CALL_FAILED, "invalid sink address from arc200_exchange"); + } + return { exchangeAsset, sink }; +} + +/** + * Read XCHG-1 `arc200_exchange()` — ASA id paired with this ARC-200 and sink address. + * + * @param {string} networkId + * @param {number} appId + * @returns {Promise<{ exchangeAsset: number, sink: string, xchgInterfaceId: string }>} + */ +export async function arc200Exchange(networkId, appId) { + const { algod, indexer } = getClients(networkId); + const ci = new ArccjsContract( + appId, + algod, + indexer, + ARC200_XCHG_ABI, + { addr: oneAddress }, + true, + false, + false + ); + ci.setFee(2000); + const res = await ci.arc200_exchange(); + if (!res?.success) { + throw new AssetMcpError( + ErrorCodes.CONTRACT_CALL_FAILED, + "arc200_exchange failed (contract may not implement XCHG-1)", + res?.error + ); + } + const { exchangeAsset, sink } = normalizeArc200ExchangeResult(res.returnValue); + return { + exchangeAsset, + sink, + xchgInterfaceId: ARC200_XCHG_INTERFACE_ID, + }; +} + +/** + * XCHG-1 `arc200_redeem`: ASA axfer (amount ≥ arg) to the app, then `arc200_redeem(amount)`. + * Uses arccjs extra-txn layout (axfer + app call) with ABI-encoded app args. + * + * @param {string} networkId + * @param {number} appId + * @param {string} sender + * @param {string} amount Base units (string) + * @returns {Promise} Unsigned txns as base64 + */ +export async function arc200RedeemTxn(networkId, appId, sender, amount) { + assertAddress(sender); + const amt = parseUintString(amount); + const { exchangeAsset } = await arc200Exchange(networkId, appId); + const { algod, indexer } = getClients(networkId); + const appAddr = algosdk.getApplicationAddress(appId); + const iface = getArc200XchgIface(); + const method = iface.getMethodByName("arc200_redeem"); + const appArgs = [ + method.getSelector(), + ...[amt].map((v, i) => method.args[i].type.encode(v)), + ]; + + const ci = new ArccjsContract( + appId, + algod, + indexer, + ARC200_XCHG_ABI, + { addr: sender }, + true, + false, + false + ); + ci.setFee(2000); + ci.setExtraTxns([ + { + xaid: exchangeAsset, + snd: sender, + arcv: appAddr.toString(), + xamt: amt, + appIndex: appId, + appArgs, + sender, + }, + ]); + + const redeemRes = await ci.arc200_redeem(amt); + if (!redeemRes?.success || !Array.isArray(redeemRes.txns) || redeemRes.txns.length === 0) { + throw new AssetMcpError( + ErrorCodes.TX_BUILD_FAILED, + "arc200_redeem build failed (check ASA balance, opt-in, and XCHG-1 support)", + redeemRes?.error + ); + } + + const txObjs = redeemRes.txns.map((b64) => + algosdk.decodeUnsignedTransaction(Buffer.from(b64, "base64")) + ); + const group = algosdk.assignGroupID(txObjs); + return group.map((txn) => + Buffer.from(algosdk.encodeUnsignedTransaction(txn)).toString("base64") + ); +} + +/** + * XCHG-1 `arc200_swapBack`: ARC-200 transfer to `sink`, then `arc200_swapBack(amount)`. + * + * @param {string} networkId + * @param {number} appId + * @param {string} sender + * @param {string} amount Base units (string) + * @returns {Promise} Unsigned txns as base64 + */ +export async function arc200SwapBackTxn(networkId, appId, sender, amount) { + assertAddress(sender); + const amt = parseUintString(amount); + const { sink } = await arc200Exchange(networkId, appId); + const transferTxns = await arc200TransferTxn(networkId, appId, sender, sink, amount); + const { algod, indexer } = getClients(networkId); + + const ci = new ArccjsContract( + appId, + algod, + indexer, + ARC200_XCHG_ABI, + { addr: sender }, + true, + false, + false + ); + ci.setFee(2000); + const swapRes = await ci.arc200_swapBack(amt); + if (!swapRes?.success || !Array.isArray(swapRes.txns) || swapRes.txns.length === 0) { + throw new AssetMcpError( + ErrorCodes.TX_BUILD_FAILED, + "arc200_swapBack build failed (check ARC-200 balance and XCHG-1 support)", + swapRes?.error + ); + } + + const decoded = [ + ...transferTxns.map((b64) => + algosdk.decodeUnsignedTransaction(Buffer.from(b64, "base64")) + ), + ...swapRes.txns.map((b64) => + algosdk.decodeUnsignedTransaction(Buffer.from(b64, "base64")) + ), + ]; + const group = algosdk.assignGroupID(decoded); + return group.map((txn) => + Buffer.from(algosdk.encodeUnsignedTransaction(txn)).toString("base64") + ); +} diff --git a/lib/tools.js b/lib/tools.js index b2b2046..40195a4 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -494,6 +494,66 @@ export function registerAssetMcpTools(server) { }) ); + reg( + "arc200_exchange", + "Read XCHG-1 arc200_exchange() — paired ASA id and sink address for ARC-200 ↔ ASA exchange (simulation). Fails if the contract does not implement the extension.", + z.object({ + network: NetworkZ, + appId: AppIdZ, + }), + (a) => arc200.arc200Exchange(a.network, a.appId), + (a, _d) => ({ + network: a.network, + standard: "arc200", + method: "arc200_exchange", + appId: a.appId, + }) + ); + + reg( + "arc200_redeem_txn", + "XCHG-1 arc200_redeem: build unsigned axfer (ASA → app) + redeem app call for base units amount. User must hold the exchange ASA and be opted in.", + z.object({ + network: NetworkZ, + appId: AppIdZ, + sender: AddressZ.describe("Account that sends ASA and signs the group."), + amount: AmountZ.describe("ASA amount in base units to redeem (must be ≤ axfer amount)."), + }), + (a) => + arc200 + .arc200RedeemTxn(a.network, a.appId, a.sender, a.amount) + .then((txns) => ({ transactions: txns })), + (a, d) => ({ + network: a.network, + standard: "arc200", + method: "arc200_redeem_txn", + appId: a.appId, + txCount: /** @type {{transactions:string[]}} */ (d).transactions.length, + }) + ); + + reg( + "arc200_swap_back_txn", + "XCHG-1 arc200_swapBack: build unsigned ARC-200 transfer to sink + swapBack app call. Converts ARC-200 back to the configured ASA from the app account.", + z.object({ + network: NetworkZ, + appId: AppIdZ, + sender: AddressZ.describe("Account that sends ARC-200 and signs the group."), + amount: AmountZ.describe("ARC-200 amount in base units to swap back."), + }), + (a) => + arc200 + .arc200SwapBackTxn(a.network, a.appId, a.sender, a.amount) + .then((txns) => ({ transactions: txns })), + (a, d) => ({ + network: a.network, + standard: "arc200", + method: "arc200_swap_back_txn", + appId: a.appId, + txCount: /** @type {{transactions:string[]}} */ (d).transactions.length, + }) + ); + // ——— ARC-72 ——— reg( "arc72_get_metadata", diff --git a/package.json b/package.json index 15efee8..8a34d18 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ }, "scripts": { "start": "node index.js", - "demo": "node examples/demo.mjs" + "demo": "node examples/demo.mjs", + "test": "node test/arc200-xchg.mjs" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.27.0", diff --git a/test/arc200-xchg.mjs b/test/arc200-xchg.mjs new file mode 100644 index 0000000..8a0815a --- /dev/null +++ b/test/arc200-xchg.mjs @@ -0,0 +1,29 @@ +/** + * Smoke tests for XCHG-1 helpers (ABI shape + encoding). No live XCHG contract required. + */ +import assert from "node:assert/strict"; +import algosdk from "algosdk"; +import { + ARC200_XCHG_ABI, + ARC200_XCHG_INTERFACE_ID, +} from "../lib/arc200.js"; + +const iface = new algosdk.ABIContract(ARC200_XCHG_ABI); + +const ex = iface.getMethodByName("arc200_exchange"); +assert.equal(Buffer.from(ex.getSelector()).toString("hex").length, 8); + +const redeem = iface.getMethodByName("arc200_redeem"); +const amt = 1_000_000n; +const redeemArgs = [redeem.getSelector(), ...redeem.args.map((a, i) => a.type.encode([amt][i]))]; +assert.equal(redeemArgs.length, 2); +assert.ok(redeemArgs[0] instanceof Uint8Array); +assert.ok(redeemArgs[1] instanceof Uint8Array); + +const swap = iface.getMethodByName("arc200_swapBack"); +const swapArgs = [swap.getSelector(), ...swap.args.map((a, i) => a.type.encode([amt][i]))]; +assert.equal(swapArgs.length, 2); + +assert.equal(ARC200_XCHG_INTERFACE_ID, "f7bde749"); + +console.log("arc200-xchg: OK");