diff --git a/.github/workflows/full_check.yaml b/.github/workflows/full_check.yaml index a7edcc00..03e6f377 100644 --- a/.github/workflows/full_check.yaml +++ b/.github/workflows/full_check.yaml @@ -137,7 +137,7 @@ jobs: coin: [monero, wownero] runs-on: ubuntu-latest container: - image: debian:bookworm + image: debian:bullseye steps: - name: Install dependencies run: | @@ -698,6 +698,123 @@ jobs: with: name: ${{ matrix.coin }} xmruw apk path: unnamed_monero_wallet/build/app/outputs/flutter-apk/*.apk + bulk_lib_release: + name: create single release file + runs-on: ubuntu-latest + needs: [ + lib_mingw, lib_android, lib_linux, lib_sailfishos_aarch64, lib_sailfishos_i486, lib_darwin, lib_macos, lib_ios + ] + steps: + - uses: actions/download-artifact@v4 + with: + name: android monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: android wownero + path: release/wownero + - uses: actions/download-artifact@v4 + with: + name: darwin monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: darwin wownero + path: release/wownero + - uses: actions/download-artifact@v4 + with: + name: ios monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: ios wownero + path: release/wownero + - uses: actions/download-artifact@v4 + with: + name: linux monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: linux wownero + path: release/wownero + - uses: actions/download-artifact@v4 + with: + name: macos monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: macos wownero + path: release/wownero + - uses: actions/download-artifact@v4 + with: + name: mingw monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: mingw wownero + path: release/wownero + - uses: actions/download-artifact@v4 + with: + name: sfos aarch64 monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: sfos aarch64 wownero + path: release/wownero + - uses: actions/download-artifact@v4 + with: + name: sfos_i486 monero + path: release/monero + - uses: actions/download-artifact@v4 + with: + name: sfos_i486 wownero + path: release/wownero + - name: zip release dir + run: zip -r release-bundle.zip release + - name: Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: release-bundle.zip + token: ${{ secrets.CUSTOM_GITHUB_TOKEN }} + - name: Upload lib + uses: actions/upload-artifact@v4 + with: + name: release-bulk + path: release + deno_monerots_test_linux: + name: test ts library + runs-on: ubuntu-24.04 + needs: [ + lib_linux + ] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: recursive + - uses: actions/download-artifact@v4 + with: + name: linux monero + path: release/monero + - name: unpack and move monero_c + run: | + unxz -f -k release/*/*.xz + - uses: denoland/setup-deno@v1 + with: + deno-version: vx.x.x + - name: Create symlink + run: | + cd impls/monero.ts + mkdir lib + cd lib + ln -s ../../../release/monero/x86_64-linux-gnu_libwallet2_api_c.so + mv x86_64-linux-gnu_libwallet2_api_c.so monero_libwallet2_api_c.so + cd .. + - name: Run tests + run: | + cd impls/monero.ts + deno run --unstable-ffi --allow-ffi checksum.ts comment_pr: name: comment on pr @@ -752,4 +869,4 @@ jobs: with: issue-number: ${{steps.get_issue_number.outputs.result}} body: | - [download artifacts #${{github.run_id}}](https://nightly.link/MrCyjaneK/monero_c/actions/runs/${{github.run_id}}) (this comment will update whenever you push) \ No newline at end of file + [download artifacts #${{github.run_id}}](https://nightly.link/MrCyjaneK/monero_c/actions/runs/${{github.run_id}}) (this comment will update whenever you push) diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b943dbc7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/generate_checksum.sh b/generate_checksum.sh index 5847ff51..4b82e53d 100755 --- a/generate_checksum.sh +++ b/generate_checksum.sh @@ -1,6 +1,11 @@ #!/bin/bash cd "$(realpath $(dirname $0))" -function sha256sum() { shasum -a 256 "$@" ; } && export -f sha256sum + +if [[ "$(uname)" == "Darwin" ]]; +then + function sha256sum() { shasum -a 256 "$@" ; } && export -f sha256sum +fi + for coin in monero wownero; do submodule_hash=$(git ls-tree HEAD ${coin} | xargs | awk '{ print $3 }') @@ -22,6 +27,13 @@ EOF const String wallet2_api_c_h_sha256 = "${COIN_wallet2_api_c_h_sha256}"; const String wallet2_api_c_cpp_sha256 = "${COIN_wallet2_api_c_cpp_sha256}"; const String wallet2_api_c_exp_sha256 = "${COIN_wallet2_api_c_exp_sha256}"; +EOF + cat > impls/monero.ts/checksum_${coin}.ts << EOF +export const ${coin}Checksum = { + wallet2_api_c_h_sha256: "${COIN_wallet2_api_c_h_sha256}", + wallet2_api_c_cpp_sha256: "${COIN_wallet2_api_c_cpp_sha256}", + wallet2_api_c_exp_sha256: "${COIN_wallet2_api_c_exp_sha256}", +} EOF done diff --git a/impls/monero.ts/.gitignore b/impls/monero.ts/.gitignore new file mode 100644 index 00000000..8b61d3b7 --- /dev/null +++ b/impls/monero.ts/.gitignore @@ -0,0 +1,2 @@ +*_libwallet2_api_c.* +lib diff --git a/impls/monero.ts/README.md b/impls/monero.ts/README.md new file mode 100644 index 00000000..e3b20f6b --- /dev/null +++ b/impls/monero.ts/README.md @@ -0,0 +1,42 @@ +# monero.ts + +`monero_c` bindings for Deno. + +## Usage + +This library does not ship with `monero_c` libraries.\ +To use these bindings you have to bring your own `monero_c` libraries.\ +There are at least two ways to do so: +- Ahead-of-time, during builds where you only ship necessary library for a given platform.\ + See [monero-tui](https://github.com/Im-Beast/monero-tui/blob/main/.github/workflows/dev-build.yml) build workflow as an example of doing so. + ```ts + import { loadDylib, Wallet, WalletManager } from "https://raw.githubusercontent.com/MrCyjaneK/monero_c/master/impls/monero.ts/mod.ts"; + + // Try to load dylib from the default lib/* path + loadDylib(); + + const wm = await WalletManager.new(); + const wallet = await Wallet.create(wm, "./my_wallet", "password"); + + console.log(await wallet.address()); + + await wallet.store(); + ``` +- Just-in-time, where you download and cache the library at runtime.\ + You can use something like [plug](https://jsr.io/@denosaurs/plug) to achieve the result. + ```ts + import { dlopen } from "jsr:@denosaurs/plug"; + // It's recommened to put the monero.ts github link into your import_map to reduce the url clutter + import { loadDylib, symbols, Wallet, WalletManager } from "https://raw.githubusercontent.com/MrCyjaneK/monero_c/master/impls/monero.ts/mod.ts"; + + // Load dylib loaded by plug + const lib = await dlopen(..., symbols); + loadDylib(lib); + + const wm = await WalletManager.new(); + const wallet = await Wallet.create(wm, "./my_wallet", "password"); + + console.log(await wallet.address()); + + await wallet.store(); + ``` diff --git a/impls/monero.ts/checksum.ts b/impls/monero.ts/checksum.ts new file mode 100644 index 00000000..22d30383 --- /dev/null +++ b/impls/monero.ts/checksum.ts @@ -0,0 +1,65 @@ +import { moneroChecksum } from "./checksum_monero.ts"; +import { readCString } from "./src/utils.ts"; +import { dylib, loadDylib } from "./src/bindings.ts"; + +loadDylib(); + +export class ChecksumError extends Error { + readonly code: number; + readonly errors: string[]; + + constructor(code: number, errors: string[]) { + super("MoneroC binding checksum failed:\n" + errors.join("\n")); + this.code = code; + this.errors = errors; + } +} + +/** + * Validates MoneroC checksums + * @returns {null} if checksums are correct + * @returns {ChecksumError} which contains information about why checksum failed + */ +export async function validateChecksum(): Promise { + const cppHeaderHash = await readCString(await dylib.symbols.MONERO_checksum_wallet2_api_c_h(), false); + const tsHeaderHash = moneroChecksum.wallet2_api_c_h_sha256; + + const errors: string[] = []; + + let errorCode = 0; + if (cppHeaderHash !== tsHeaderHash) { + errors.push("ERR: Header file check mismatch"); + errorCode++; + } + + const cppSourceHash = await readCString(await dylib.symbols.MONERO_checksum_wallet2_api_c_cpp(), false); + const tsSourceHash = moneroChecksum.wallet2_api_c_cpp_sha256; + if (cppSourceHash !== tsSourceHash) { + errors.push(`ERR: CPP source file check mismatch ${cppSourceHash} == ${tsSourceHash}`); + errorCode++; + } + + const cppExportHash = await readCString(await dylib.symbols.MONERO_checksum_wallet2_api_c_exp(), false); + const tsExportHash = moneroChecksum.wallet2_api_c_exp_sha256; + if (cppExportHash !== tsExportHash) { + if (Deno.build.os !== "darwin") { + errors.push("WARN: EXP source file check mismatch"); + } else { + errors.push(`ERR: EXP source file check mismatch ${cppExportHash} == ${tsExportHash}`); + } + errorCode++; + } + + if (errorCode) { + return new ChecksumError(errorCode, errors); + } + + return null; +} + +if (import.meta.main) { + const maybeError = await validateChecksum(); + if (maybeError) { + throw maybeError; + } +} diff --git a/impls/monero.ts/checksum_monero.ts b/impls/monero.ts/checksum_monero.ts new file mode 100644 index 00000000..88406a0c --- /dev/null +++ b/impls/monero.ts/checksum_monero.ts @@ -0,0 +1,5 @@ +export const moneroChecksum = { + wallet2_api_c_h_sha256: "e8db0ef0324a153f5e3ecca4c0db23c54f4576e84988f04bd4f11c1142f9d7ad", + wallet2_api_c_cpp_sha256: "dca52ac9ee009fda9fb5726543a454885e61d8eb74fb33112288029ed625bec5-b089f9ee69924882c5d14dd1a6991deb05d9d1cd", + wallet2_api_c_exp_sha256: "c8913ac41068f67b57c9b0a3c7dd8973e3c1273b66c2ff0aadb0003931da748c", +} diff --git a/impls/monero.ts/checksum_wownero.ts b/impls/monero.ts/checksum_wownero.ts new file mode 100644 index 00000000..8b2899c6 --- /dev/null +++ b/impls/monero.ts/checksum_wownero.ts @@ -0,0 +1,5 @@ +export const wowneroChecksum = { + wallet2_api_c_h_sha256: "8a8d386dd5d996c89a0586c55b295ef95ca584bf1ffa26255152b291910a0a77", + wallet2_api_c_cpp_sha256: "07d67f34a07869aaa4af6ca04e142dbad2fb1fba0e2ebdefd22bc333fd982e25-e25963cbc11ca0a0fe5eb34b9bd7c72e4f51b795", + wallet2_api_c_exp_sha256: "3673e40e1a7115552276d1d541f6e4d5a0fef47c40fff7b988f49923af84c8a4", +} diff --git a/impls/monero.ts/deno.jsonc b/impls/monero.ts/deno.jsonc new file mode 100644 index 00000000..a7b75eca --- /dev/null +++ b/impls/monero.ts/deno.jsonc @@ -0,0 +1,5 @@ +{ + "fmt": { + "lineWidth": 120 + } +} diff --git a/impls/monero.ts/mod.ts b/impls/monero.ts/mod.ts new file mode 100644 index 00000000..1eca7734 --- /dev/null +++ b/impls/monero.ts/mod.ts @@ -0,0 +1,6 @@ +export * from "./src/bindings.ts"; +export * from "./src/pending_transaction.ts"; +export * from "./src/transaction_history.ts"; +export * from "./src/transaction_info.ts"; +export * from "./src/wallet.ts"; +export * from "./src/wallet_manager.ts"; diff --git a/impls/monero.ts/src/bindings.ts b/impls/monero.ts/src/bindings.ts new file mode 100644 index 00000000..b854f7d3 --- /dev/null +++ b/impls/monero.ts/src/bindings.ts @@ -0,0 +1,562 @@ +export const symbols = { + "MONERO_WalletManagerFactory_getWalletManager": { + nonblocking: true, + parameters: [], + // void* + result: "pointer", + }, + + //#region WalletManager + "MONERO_WalletManager_createWallet": { + nonblocking: true, + // void* wm_ptr, const char* path, const char* password, const char* language, int networkType + parameters: ["pointer", "pointer", "pointer", "pointer", "i32"], + // void* + result: "pointer", + }, + "MONERO_WalletManager_openWallet": { + nonblocking: true, + // void* wm_ptr, const char* path, const char* password, int networkType + "parameters": ["pointer", "pointer", "pointer", "i32"], + // void* + result: "pointer", + }, + "MONERO_WalletManager_recoveryWallet": { + nonblocking: true, + // void* wm_ptr, const char* path, const char* password, const char* mnemonic, + // int networkType, uint64_t restoreHeight, uint64_t kdfRounds, const char* seedOffset + parameters: ["pointer", "pointer", "pointer", "pointer", "i32", "u64", "u64", "pointer"], + // void* + result: "pointer", + }, + "MONERO_WalletManager_blockchainHeight": { + nonblocking: true, + // void* wm_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_WalletManager_blockchainTargetHeight": { + nonblocking: true, + // void* wm_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_WalletManager_setDaemonAddress": { + nonblocking: true, + // void* wm_ptr, const char* address + parameters: ["pointer", "pointer"], + // void + result: "void", + }, + //#endregion + + //#region Wallet + "MONERO_Wallet_init": { + nonblocking: true, + // void* wallet_ptr, const char* daemon_address, uint64_t upper_transaction_size_limit, + // const char* daemon_username, const char* daemon_password, bool use_ssl, bool lightWallet, + // const char* proxy_address + parameters: ["pointer", "pointer", "u64", "pointer", "pointer", "bool", "bool", "pointer"], + // bool + result: "bool", + }, + "MONERO_Wallet_init3": { + nonblocking: true, + // void* wallet_ptr, const char* argv0, const char* default_log_base_name, + // const char* log_path, bool console + parameters: ["pointer", "pointer", "pointer", "pointer", "bool"], + // void + result: "void", + }, + "MONERO_Wallet_setTrustedDaemon": { + nonblocking: true, + // void* wallet_ptr, bool arg + parameters: ["pointer", "bool"], + // void + result: "void", + }, + "MONERO_Wallet_startRefresh": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // void + result: "void", + }, + "MONERO_Wallet_refreshAsync": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // void + result: "void", + }, + "MONERO_Wallet_blockChainHeight": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_Wallet_daemonBlockChainHeight": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_Wallet_synchronized": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // bool + result: "bool", + }, + "MONERO_Wallet_store": { + nonblocking: true, + // void* wallet_ptr, const char* path + parameters: ["pointer", "pointer"], + // bool + result: "bool", + }, + "MONERO_Wallet_address": { + nonblocking: true, + // void* wallet_ptr, uint64_t accountIndex, uint64_t addressIndex + parameters: ["pointer", "u64", "u64"], + // char* + result: "pointer", + }, + "MONERO_Wallet_balance": { + nonblocking: true, + // void* wallet_ptr, uint32_t accountIndex + parameters: ["pointer", "u32"], + // uint64_t + result: "u64", + }, + "MONERO_Wallet_unlockedBalance": { + nonblocking: true, + // void* wallet_ptr, uint32_t accountIndex + parameters: ["pointer", "u32"], + // uint64_t + result: "u64", + }, + "MONERO_Wallet_addSubaddressAccount": { + nonblocking: true, + // void* wallet_ptr, const char* label + parameters: ["pointer", "pointer"], + // void + result: "void", + }, + "MONERO_Wallet_numSubaddressAccounts": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // size_t + result: "usize", + }, + "MONERO_Wallet_addSubaddress": { + nonblocking: true, + // void* wallet_ptr, uint32_t accountIndex, const char* label + parameters: ["pointer", "u32", "pointer"], + // void + result: "void", + }, + "MONERO_Wallet_numSubaddresses": { + nonblocking: true, + // void* wallet_ptr, uint32_t accountIndex + parameters: ["pointer", "u32"], + // size_t + result: "usize", + }, + "MONERO_Wallet_getSubaddressLabel": { + nonblocking: true, + // void* wallet_ptr, uint32_t accountIndex, uint32_t addressIndex + parameters: ["pointer", "u32", "u32"], + // const char* + result: "pointer", + }, + "MONERO_Wallet_setSubaddressLabel": { + nonblocking: true, + // void* wallet_ptr, uint32_t accountIndex, uint32_t addressIndex, const char* label + parameters: ["pointer", "u32", "u32", "pointer"], + // void + result: "void", + }, + "MONERO_Wallet_status": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // int + result: "i32", + }, + "MONERO_Wallet_errorString": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // char* + result: "pointer", + }, + "MONERO_Wallet_history": { + nonblocking: true, + // void* wallet_ptr + parameters: ["pointer"], + // void* + result: "pointer", + }, + "MONERO_Wallet_createTransaction": { + nonblocking: true, + // void* wallet_ptr, const char* dst_addr, const char* payment_id + // uint64_t amount, uint32_t mixin_count, int pendingTransactionPriority, + // uint32_t subaddr_account, const char* preferredInputs, const char* separator + parameters: ["pointer", "pointer", "pointer", "u64", "u32", "i32", "u32", "pointer", "pointer"], + // void* + result: "pointer", + }, + "MONERO_Wallet_amountFromString": { + nonblocking: true, + // const char* amount + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + //#endregion + + //#region TransactionHistory + "MONERO_TransactionHistory_count": { + nonblocking: true, + // void* txHistory_ptr + parameters: ["pointer"], + // int + result: "i32", + }, + "MONERO_TransactionHistory_transaction": { + nonblocking: true, + // void* txHistory_ptr, int index + parameters: ["pointer", "i32"], + // void* + result: "pointer", + }, + "MONERO_TransactionHistory_transactionById": { + nonblocking: true, + // void* txHistory_ptr, const char* id + parameters: ["pointer", "pointer"], + // void* + result: "pointer", + }, + "MONERO_TransactionHistory_refresh": { + nonblocking: true, + // void* txHistory_ptr + parameters: ["pointer"], + // void + result: "void", + }, + "MONERO_TransactionHistory_setTxNote": { + nonblocking: true, + // void* txHistory_ptr, const char* txid, const char* note + parameters: ["pointer", "pointer", "pointer"], + // void + result: "void", + }, + //#endregion + + //#region TransactionInfo + "MONERO_TransactionInfo_direction": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // int + result: "i32", + }, + "MONERO_TransactionInfo_isPending": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // bool + result: "bool", + }, + "MONERO_TransactionInfo_isFailed": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // bool + result: "bool", + }, + "MONERO_TransactionInfo_isCoinbase": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // bool + result: "bool", + }, + "MONERO_TransactionInfo_amount": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_TransactionInfo_fee": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_TransactionInfo_blockHeight": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_TransactionInfo_description": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_TransactionInfo_subaddrIndex": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_TransactionInfo_subaddrAccount": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // uint32_t + result: "u32", + }, + "MONERO_TransactionInfo_label": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_TransactionInfo_confirmations": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_TransactionInfo_unlockTime": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_TransactionInfo_hash": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_TransactionInfo_timestamp": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_TransactionInfo_paymentId": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_TransactionInfo_transfers_count": { + nonblocking: true, + // void* txInfo_ptr + parameters: ["pointer"], + // int + result: "i32", + }, + "MONERO_TransactionInfo_transfers_amount": { + nonblocking: true, + // void* txInfo_ptr, int index + parameters: ["pointer", "i32"], + // uint64_t + result: "u64", + }, + "MONERO_TransactionInfo_transfers_address": { + nonblocking: true, + // void* txInfo_ptr, int index + parameters: ["pointer", "i32"], + // const char* + result: "pointer", + }, + //#endregion + + //#region PendingTransaction + "MONERO_PendingTransaction_status": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // int + result: "i32", + }, + "MONERO_PendingTransaction_errorString": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_PendingTransaction_commit": { + nonblocking: true, + // void* pendingTx_ptr, const char* filename, bool overwrite + parameters: ["pointer", "pointer", "bool"], + // bool + result: "bool", + }, + "MONERO_PendingTransaction_commitUR": { + nonblocking: true, + // void* pendingTx_ptr, int max_fragment_length + parameters: ["pointer", "i32"], + // const char* + result: "pointer", + }, + "MONERO_PendingTransaction_amount": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_PendingTransaction_dust": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_PendingTransaction_fee": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_PendingTransaction_txid": { + nonblocking: true, + // void* pendingTx_ptr, const char* separator + parameters: ["pointer", "pointer"], + // const char* + result: "pointer", + }, + "MONERO_PendingTransaction_txCount": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // uint64_t + result: "u64", + }, + "MONERO_PendingTransaction_subaddrAccount": { + nonblocking: true, + // void* pendingTx_ptr, const char* separator + parameters: ["pointer", "pointer"], + // const char* + result: "pointer", + }, + "MONERO_PendingTransaction_subaddrIndices": { + nonblocking: true, + // void* pendingTx_ptr, const char* separator + parameters: ["pointer", "pointer"], + // const char* + result: "pointer", + }, + "MONERO_PendingTransaction_multisigSignData": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_PendingTransaction_signMultisigTx": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // void + result: "void", + }, + "MONERO_PendingTransaction_signersKeys": { + nonblocking: true, + // void* pendingTx_ptr + parameters: ["pointer"], + // const char* + result: "pointer", + }, + "MONERO_PendingTransaction_hex": { + nonblocking: true, + // void* pendingTx_ptr, const char* separator + parameters: ["pointer", "pointer"], + // const char* + result: "pointer", + }, + //#endregion + + //#region Checksum + "MONERO_checksum_wallet2_api_c_h": { + nonblocking: true, + parameters: [], + // const char* + result: "pointer", + }, + "MONERO_checksum_wallet2_api_c_cpp": { + nonblocking: true, + parameters: [], + // const char* + result: "pointer", + }, + "MONERO_checksum_wallet2_api_c_exp": { + nonblocking: true, + parameters: [], + // const char* + result: "pointer", + }, + //#endregion + + "MONERO_free": { + nonblocking: true, + // void* ptr + parameters: ["pointer"], + // void + result: "void", + }, +} as const; + +type MoneroTsDylib = Deno.DynamicLibrary; + +export let dylib: MoneroTsDylib; +export function loadDylib(newDylib?: MoneroTsDylib) { + if (newDylib) { + dylib = newDylib; + return; + } + + let libPath: string; + switch (Deno.build.os) { + case "darwin": + libPath = "./lib/monero_libwallet2_api_c.dylib"; + break; + case "android": + libPath = "./lib/libmonero_libwallet2_api_c.so"; + break; + case "windows": + libPath = "./lib/monero_libwallet2_api_c.dll"; + break; + default: + libPath = "./lib/monero_libwallet2_api_c.so"; + break; + } + + dylib = Deno.dlopen(libPath, symbols); +} diff --git a/impls/monero.ts/src/pending_transaction.ts b/impls/monero.ts/src/pending_transaction.ts new file mode 100644 index 00000000..cf487218 --- /dev/null +++ b/impls/monero.ts/src/pending_transaction.ts @@ -0,0 +1,81 @@ +import { dylib } from "./bindings.ts"; +import { CString, readCString, type Sanitizer } from "./utils.ts"; + +export type PendingTransactionPtr = Deno.PointerObject<"transactionInfo">; + +export class PendingTransaction { + #pendingTxPtr: PendingTransactionPtr; + sanitizer?: Sanitizer; + + constructor(pendingTxPtr: PendingTransactionPtr, sanitizer?: Sanitizer) { + this.sanitizer = sanitizer; + this.#pendingTxPtr = pendingTxPtr; + } + + async status(): Promise { + return await dylib.symbols.MONERO_PendingTransaction_status(this.#pendingTxPtr); + } + + async errorString(): Promise { + if (!await this.status()) return null; + + const error = await dylib.symbols.MONERO_PendingTransaction_errorString(this.#pendingTxPtr); + if (!error) return null; + + return await readCString(error) || null; + } + + async throwIfError(sanitize = true): Promise { + const maybeError = await this.errorString(); + if (maybeError) { + if (sanitize) this.sanitizer?.(); + throw new Error(maybeError); + } + } + + async commit(fileName: string, overwrite: boolean, sanitize = true): Promise { + const bool = await dylib.symbols.MONERO_PendingTransaction_commit( + this.#pendingTxPtr, + CString(fileName), + overwrite, + ); + await this.throwIfError(sanitize); + return bool; + } + + async commitUR(maxFragmentLength: number): Promise { + const result = await dylib.symbols.MONERO_PendingTransaction_commitUR( + this.#pendingTxPtr, + maxFragmentLength, + ); + if (!result) return null; + await this.throwIfError(); + return await readCString(result) || null; + } + + async amount(): Promise { + return await dylib.symbols.MONERO_PendingTransaction_amount(this.#pendingTxPtr); + } + + async dust(): Promise { + return await dylib.symbols.MONERO_PendingTransaction_dust(this.#pendingTxPtr); + } + + async fee(): Promise { + return await dylib.symbols.MONERO_PendingTransaction_fee(this.#pendingTxPtr); + } + + async txid(separator: string, sanitize = true): Promise { + const result = await dylib.symbols.MONERO_PendingTransaction_txid( + this.#pendingTxPtr, + CString(separator), + ); + if (!result) return null; + await this.throwIfError(sanitize); + return await readCString(result) || null; + } + + async txCount(): Promise { + return await dylib.symbols.MONERO_PendingTransaction_txCount(this.#pendingTxPtr); + } +} diff --git a/impls/monero.ts/src/transaction_history.ts b/impls/monero.ts/src/transaction_history.ts new file mode 100644 index 00000000..cc76fc25 --- /dev/null +++ b/impls/monero.ts/src/transaction_history.ts @@ -0,0 +1,38 @@ +import { dylib } from "./bindings.ts"; +import { TransactionInfo, TransactionInfoPtr } from "./transaction_info.ts"; +import { CString } from "./utils.ts"; + +export type TransactionHistoryPtr = Deno.PointerObject<"transactionHistory">; + +export class TransactionHistory { + #txHistoryPtr: TransactionHistoryPtr; + + constructor(txHistoryPtr: TransactionHistoryPtr) { + this.#txHistoryPtr = txHistoryPtr; + } + + async count(): Promise { + return await dylib.symbols.MONERO_TransactionHistory_count(this.#txHistoryPtr); + } + + async transaction(index: number): Promise { + return new TransactionInfo( + (await dylib.symbols.MONERO_TransactionHistory_transaction( + this.#txHistoryPtr, + index, + )) as TransactionInfoPtr, + ); + } + + async refresh(): Promise { + await dylib.symbols.MONERO_TransactionHistory_refresh(this.#txHistoryPtr); + } + + async setTxNote(transactionId: string, note: string): Promise { + await dylib.symbols.MONERO_TransactionHistory_setTxNote( + this.#txHistoryPtr, + CString(transactionId), + CString(note), + ); + } +} diff --git a/impls/monero.ts/src/transaction_info.ts b/impls/monero.ts/src/transaction_info.ts new file mode 100644 index 00000000..7db45f5d --- /dev/null +++ b/impls/monero.ts/src/transaction_info.ts @@ -0,0 +1,104 @@ +import { dylib } from "./bindings.ts"; +import { readCString, Sanitizer } from "./utils.ts"; + +export type TransactionInfoPtr = Deno.PointerObject<"transactionInfo">; + +export class TransactionInfo { + #txInfoPtr: TransactionInfoPtr; + sanitizer?: Sanitizer; + + constructor(txInfoPtr: TransactionInfoPtr, sanitizer?: Sanitizer) { + this.#txInfoPtr = txInfoPtr; + this.sanitizer = sanitizer; + } + + async direction(): Promise<"in" | "out"> { + switch (await dylib.symbols.MONERO_TransactionInfo_direction(this.#txInfoPtr)) { + case 0: + return "in"; + case 1: + return "out"; + default: + await this.sanitizer?.(); + throw new Error("Invalid TransactionInfo direction"); + } + } + + async isPending(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_isPending(this.#txInfoPtr); + } + + async isFailed(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_isFailed(this.#txInfoPtr); + } + + async isCoinbase(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_isCoinbase(this.#txInfoPtr); + } + + async amount(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_amount(this.#txInfoPtr); + } + + async fee(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_fee(this.#txInfoPtr); + } + + async blockHeight(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_blockHeight(this.#txInfoPtr); + } + + async description(): Promise { + const description = await dylib.symbols.MONERO_TransactionInfo_description(this.#txInfoPtr); + return await readCString(description) || ""; + } + + async subaddrIndex(): Promise { + const subaddrIndex = await dylib.symbols.MONERO_TransactionInfo_subaddrIndex(this.#txInfoPtr); + return await readCString(subaddrIndex) || ""; + } + + async subaddrAccount(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_subaddrAccount(this.#txInfoPtr); + } + + async label(): Promise { + const label = await dylib.symbols.MONERO_TransactionInfo_label(this.#txInfoPtr); + return await readCString(label) || ""; + } + + async confirmations(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_confirmations(this.#txInfoPtr); + } + + async unlockTime(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_unlockTime(this.#txInfoPtr); + } + + async hash(): Promise { + const hash = await dylib.symbols.MONERO_TransactionInfo_hash(this.#txInfoPtr); + return await readCString(hash) || ""; + } + + async timestamp(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_timestamp(this.#txInfoPtr); + } + + async paymentId(): Promise { + const paymentId = await dylib.symbols.MONERO_TransactionInfo_paymentId(this.#txInfoPtr); + return await readCString(paymentId) || ""; + } + + async transfersCount(): Promise { + return await dylib.symbols.MONERO_TransactionInfo_transfers_count(this.#txInfoPtr); + } + + async transfersAmount(index: number): Promise { + return await dylib.symbols.MONERO_TransactionInfo_transfers_amount(this.#txInfoPtr, index); + } + + async transfersAddress(index: number): Promise { + const transfersAddress = await dylib.symbols.MONERO_TransactionInfo_transfers_address(this.#txInfoPtr, index); + return await readCString(transfersAddress) || ""; + } +} diff --git a/impls/monero.ts/src/utils.ts b/impls/monero.ts/src/utils.ts new file mode 100644 index 00000000..6fa640f6 --- /dev/null +++ b/impls/monero.ts/src/utils.ts @@ -0,0 +1,25 @@ +import { dylib } from "../mod.ts"; + +export type Sanitizer = () => void | PromiseLike; + +const textEncoder = new TextEncoder(); +export function CString(string: string): Deno.PointerValue { + return Deno.UnsafePointer.of(textEncoder.encode(`${string}\x00`)); +} + +/** + * This method reads string from the given pointer and frees the string. + * + * SAFETY: Do not use readCString twice on the same pointer as it will cause double free\ + * If you want to read CString without freeing it set the {@linkcode free} parameter to false + */ +export async function readCString(pointer: Deno.PointerObject, free?: boolean): Promise; +export async function readCString(pointer: Deno.PointerValue, free?: boolean): Promise; +export async function readCString(pointer: Deno.PointerValue, free = true): Promise { + if (!pointer) return null; + const string = new Deno.UnsafePointerView(pointer).getCString(); + if (free) { + await dylib.symbols.MONERO_free(pointer); + } + return string; +} diff --git a/impls/monero.ts/src/wallet.ts b/impls/monero.ts/src/wallet.ts new file mode 100644 index 00000000..07c40ceb --- /dev/null +++ b/impls/monero.ts/src/wallet.ts @@ -0,0 +1,304 @@ +import { dylib } from "./bindings.ts"; +import { CString, readCString, Sanitizer } from "./utils.ts"; +import { WalletManager, type WalletManagerPtr } from "./wallet_manager.ts"; +import { TransactionHistory, TransactionHistoryPtr } from "./transaction_history.ts"; + +import { PendingTransaction } from "./pending_transaction.ts"; +import { PendingTransactionPtr } from "./pending_transaction.ts"; + +export type WalletPtr = Deno.PointerObject<"walletManager">; + +export class Wallet { + #walletManagerPtr: WalletManagerPtr; + #walletPtr: WalletPtr; + sanitizer?: Sanitizer; + + constructor(walletManagerPtr: WalletManager, walletPtr: WalletPtr, sanitizer?: Sanitizer) { + this.#walletPtr = walletPtr; + this.#walletManagerPtr = walletManagerPtr.getPointer(); + this.sanitizer = sanitizer; + } + + async store(path = ""): Promise { + const bool = await dylib.symbols.MONERO_Wallet_store(this.#walletPtr, CString(path)); + await this.throwIfError(); + return bool; + } + + async initWallet(): Promise { + await this.init(); + await this.setTrustedDaemon(true); + await this.setDaemonAddress("http://nodex.monerujo.io:18081"); + await this.startRefresh(); + await this.refreshAsync(); + await this.throwIfError(); + } + + async setDaemonAddress(address: string): Promise { + await dylib.symbols.MONERO_WalletManager_setDaemonAddress( + this.#walletManagerPtr, + CString(address), + ); + } + + async startRefresh(): Promise { + await dylib.symbols.MONERO_Wallet_startRefresh(this.#walletPtr); + await this.throwIfError(); + } + + async refreshAsync(): Promise { + await dylib.symbols.MONERO_Wallet_refreshAsync(this.#walletPtr); + await this.throwIfError(); + } + + async init(): Promise { + const bool = await dylib.symbols.MONERO_Wallet_init( + this.#walletPtr, + CString("http://nodex.monerujo.io:18081"), + 0n, + CString(""), + CString(""), + false, + false, + CString(""), + ); + await this.throwIfError(); + return bool; + } + + async setTrustedDaemon(value: boolean): Promise { + await dylib.symbols.MONERO_Wallet_setTrustedDaemon(this.#walletPtr, value); + } + + static async create( + walletManager: WalletManager, + path: string, + password: string, + sanitizeError = true, + ): Promise { + // We assign holder of the pointer in Wallet constructor + const walletManagerPtr = walletManager.getPointer(); + + const walletPtr = await dylib.symbols.MONERO_WalletManager_createWallet( + walletManagerPtr, + CString(path), + CString(password), + CString("English"), + 0, + ); + + const wallet = new Wallet(walletManager, walletPtr as WalletPtr, walletManager.sanitizer); + await wallet.throwIfError(sanitizeError); + await wallet.initWallet(); + + return wallet; + } + + static async open( + walletManager: WalletManager, + path: string, + password: string, + sanitizeError = true, + ): Promise { + // We assign holder of the pointer in Wallet constructor + const walletManagerPtr = walletManager.getPointer(); + + const walletPtr = await dylib.symbols.MONERO_WalletManager_openWallet( + walletManagerPtr, + CString(path), + CString(password), + 0, + ); + + const wallet = new Wallet(walletManager, walletPtr as WalletPtr, walletManager.sanitizer); + await wallet.throwIfError(sanitizeError); + await wallet.initWallet(); + + return wallet; + } + + static async recover( + walletManager: WalletManager, + path: string, + password: string, + mnemonic: string, + restoreHeight: bigint, + seedOffset: string = "", + sanitizeError = true, + ): Promise { + // We assign holder of the pointer in Wallet constructor + const walletManagerPtr = walletManager.getPointer(); + + const walletPtr = await dylib.symbols.MONERO_WalletManager_recoveryWallet( + walletManagerPtr, + CString(path), + CString(password), + CString(mnemonic), + 0, + restoreHeight, + 1n, + CString(seedOffset), + ); + + const wallet = new Wallet(walletManager, walletPtr as WalletPtr, walletManager.sanitizer); + await wallet.throwIfError(sanitizeError); + await wallet.initWallet(); + + return wallet; + } + + async address(accountIndex = 0n, addressIndex = 0n): Promise { + const address = await dylib.symbols.MONERO_Wallet_address(this.#walletPtr, accountIndex, addressIndex); + if (!address) { + const error = await this.errorString(); + throw new Error(`Failed getting address from a wallet: ${error ?? ""}`); + } + return await readCString(address); + } + + async balance(accountIndex = 0): Promise { + return await dylib.symbols.MONERO_Wallet_balance(this.#walletPtr, accountIndex); + } + + async unlockedBalance(accountIndex = 0): Promise { + return await dylib.symbols.MONERO_Wallet_unlockedBalance(this.#walletPtr, accountIndex); + } + + status(): Promise { + return dylib.symbols.MONERO_Wallet_status(this.#walletPtr); + } + + async errorString(): Promise { + if (!await this.status()) return null; + + const error = await dylib.symbols.MONERO_Wallet_errorString(this.#walletPtr); + if (!error) return null; + + return await readCString(error) || null; + } + + async throwIfError(sanitize = true): Promise { + const maybeError = await this.errorString(); + if (maybeError) { + if (sanitize) this.sanitizer?.(); + throw new Error(maybeError); + } + } + + async synchronized(): Promise { + const synchronized = await dylib.symbols.MONERO_Wallet_synchronized(this.#walletPtr); + await this.throwIfError(); + return synchronized; + } + + async blockChainHeight(): Promise { + const height = await dylib.symbols.MONERO_Wallet_blockChainHeight(this.#walletPtr); + await this.throwIfError(); + return height; + } + + async daemonBlockChainHeight(): Promise { + const height = await dylib.symbols.MONERO_Wallet_daemonBlockChainHeight(this.#walletPtr); + await this.throwIfError(); + return height; + } + + async managerBlockChainHeight(): Promise { + const height = await dylib.symbols.MONERO_WalletManager_blockchainHeight(this.#walletManagerPtr); + await this.throwIfError(); + return height; + } + + async managerTargetBlockChainHeight(): Promise { + const height = await dylib.symbols.MONERO_WalletManager_blockchainTargetHeight(this.#walletManagerPtr); + await this.throwIfError(); + return height; + } + + async addSubaddressAccount(label: string): Promise { + await dylib.symbols.MONERO_Wallet_addSubaddressAccount( + this.#walletPtr, + CString(label), + ); + await this.throwIfError(); + } + + async numSubaddressAccounts(): Promise { + const accountsLen = await dylib.symbols.MONERO_Wallet_numSubaddressAccounts(this.#walletPtr); + await this.throwIfError(); + return accountsLen; + } + + async addSubaddress(accountIndex: number, label: string): Promise { + await dylib.symbols.MONERO_Wallet_addSubaddress( + this.#walletPtr, + accountIndex, + CString(label), + ); + await this.throwIfError(); + } + + async numSubaddresses(accountIndex: number): Promise { + const address = await dylib.symbols.MONERO_Wallet_numSubaddresses( + this.#walletPtr, + accountIndex, + ); + await this.throwIfError(); + return address; + } + + async getSubaddressLabel(accountIndex: number, addressIndex: number): Promise { + const label = await dylib.symbols.MONERO_Wallet_getSubaddressLabel(this.#walletPtr, accountIndex, addressIndex); + if (!label) { + const error = await this.errorString(); + throw new Error(`Failed getting subaddress label from a wallet: ${error ?? ""}`); + } + return await readCString(label); + } + + async setSubaddressLabel(accountIndex: number, addressIndex: number, label: string): Promise { + await dylib.symbols.MONERO_Wallet_setSubaddressLabel( + this.#walletPtr, + accountIndex, + addressIndex, + CString(label), + ); + await this.throwIfError(); + } + + async getHistory(): Promise { + const transactionHistoryPointer = await dylib.symbols.MONERO_Wallet_history(this.#walletPtr); + await this.throwIfError(); + return new TransactionHistory(transactionHistoryPointer as TransactionHistoryPtr); + } + + async createTransaction( + destinationAddress: string, + amount: bigint, + pendingTransactionPriority = 0 | 1 | 2 | 3, + subaddressAccount: number, + sanitize = true, + prefferedInputs = "", + mixinCount = 0, + paymentId = "", + separator = ",", + ): Promise { + const pendingTxPtr = await dylib.symbols.MONERO_Wallet_createTransaction( + this.#walletPtr, + CString(destinationAddress), + CString(paymentId), + amount, + mixinCount, + pendingTransactionPriority, + subaddressAccount, + CString(prefferedInputs), + CString(separator), + ); + await this.throwIfError(sanitize); + return new PendingTransaction(pendingTxPtr as PendingTransactionPtr); + } + + async amountFromString(amount: string): Promise { + return await dylib.symbols.MONERO_Wallet_amountFromString(CString(amount)); + } +} diff --git a/impls/monero.ts/src/wallet_manager.ts b/impls/monero.ts/src/wallet_manager.ts new file mode 100644 index 00000000..ad9cf315 --- /dev/null +++ b/impls/monero.ts/src/wallet_manager.ts @@ -0,0 +1,27 @@ +import { dylib } from "./bindings.ts"; +import { Sanitizer } from "./utils.ts"; + +export type WalletManagerPtr = Deno.PointerObject<"walletManager">; + +export class WalletManager { + #ptr: WalletManagerPtr; + sanitizer?: Sanitizer; + + constructor(walletManagerPtr: WalletManagerPtr, sanitizer?: Sanitizer) { + this.#ptr = walletManagerPtr; + this.sanitizer = sanitizer; + } + + getPointer(): WalletManagerPtr { + return this.#ptr; + } + + static async new(sanitizer?: Sanitizer) { + const ptr = await dylib.symbols.MONERO_WalletManagerFactory_getWalletManager(); + if (!ptr) { + sanitizer?.(); + throw new Error("Failed retrieving wallet manager"); + } + return new WalletManager(ptr as WalletManagerPtr, sanitizer); + } +}