Skip to content

Commit

Permalink
Merge branch 'master' into cyjan-cleanup-patches
Browse files Browse the repository at this point in the history
MrCyjaneK authored Dec 2, 2024
2 parents fffd222 + 40c1a1b commit 8f52aeb
Showing 22 changed files with 3,820 additions and 808 deletions.
16 changes: 12 additions & 4 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
FROM ghcr.io/cirruslabs/flutter:3.24.3
# FROM debian:bookworm
SHELL ["/bin/bash", "-c"]

# Install dependencies

@@ -15,13 +16,20 @@ RUN apt update \

RUN bash -c 'if [[ "$(uname -m)" == "aarch64" || "$(uname -m)" == "arm64" ]] then sudo rm -rf /opt/android-sdk-linux/platform-tools; fi'

# Install zsh and configure it

ENV DENO_VERSION=v2.0.3
RUN if [[ "$(uname -m)" == "aarch64" || "$(uname -m)" == "arm64" ]] then export DENO_ARCH=aarch64-unknown-linux-gnu; fi; \
if [[ "$(uname -m)" == "x86_64" || "$(uname -m)" == "amd64" ]] then export DENO_ARCH=x86_64-unknown-linux-gnu; fi; \
wget -O /tmp/deno.zip "https://github.com/denoland/deno/releases/download/${DENO_VERSION}/deno-${DENO_ARCH}.zip" && \
cd /tmp && \
unzip /tmp/deno.zip && \
mv deno /usr/local/bin && \
rm -rf deno deno.zip

USER ubuntu

RUN git config --global --add safe.directory '*'
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" \
-t robbyrussell

RUN git config --global --add safe.directory '*'

ENV SUDO=sudo
ENV SUDO=sudo
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -17,7 +17,8 @@
},
"extensions": [
"dart-code.flutter",
"dart-code.dart-code"
"dart-code.dart-code",
"denoland.vscode-deno"
]
}
},
86 changes: 85 additions & 1 deletion .github/workflows/full_check.yaml
Original file line number Diff line number Diff line change
@@ -429,4 +429,88 @@ jobs:
path: release/${{ matrix.coin }}

- name: Run regression tests
run: COIN="${{ matrix.coin }}" deno test -A tests/
run: COIN="${{ matrix.coin }}" deno test -A tests/regression.test.ts

integration_check:
strategy:
matrix:
coin: [monero, wownero]
needs: [
lib_linux
]
runs-on: ubuntu-24.04
steps:
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive

- uses: actions/download-artifact@v4
with:
name: linux ${{ matrix.coin }}
path: release/${{ matrix.coin }}

- name: Run integration tests
run: COIN="${{ matrix.coin }}" deno test -A tests/integration.test.ts
env:
SECRET_WALLET_PASSWORD: ${{ secrets.SECRET_WALLET_PASSWORD }}
SECRET_WALLET_MNEMONIC: ${{ secrets.SECRET_WALLET_MNEMONIC }}
SECRET_WALLET_RESTORE_HEIGHT: ${{ secrets.SECRET_WALLET_RESTORE_HEIGHT }}


comment_pr:
name: comment on pr
runs-on: ubuntu-latest
needs: [
lib_mingw, lib_android, lib_linux, lib_macos, lib_ios,
]
steps:
- uses: actions/github-script@v7
continue-on-error: true
id: get_issue_number
with:
script: |
if (context.issue.number) {
// Return issue number if present
return context.issue.number;
} else {
// Otherwise return issue number from commit
return (
await github.rest.repos.listPullRequestsAssociatedWithCommit({
commit_sha: context.sha,
owner: context.repo.owner,
repo: context.repo.repo,
})
).data[0].number;
}
result-encoding: string
- name: Find Comment
continue-on-error: true
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{steps.get_issue_number.outputs.result}}
comment-author: 'github-actions[bot]'
body-includes: download artifacts
- name: Update comment
continue-on-error: true
if: steps.fc.outcome == 'success'
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
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}})
edit-mode: replace
- name: Create comment
continue-on-error: true
if: steps.fc.outcome == 'failure'
uses: peter-evans/create-or-update-comment@v4
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)
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
release/
build/
tests/monero-cli
tests/wownero-cli
tests/libs
tests/wallets
4 changes: 2 additions & 2 deletions impls/monero.ts/README.md
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ There are at least two ways to do so:
loadMoneroDylib();

const wm = await WalletManager.new();
const wallet = await Wallet.create(wm, "./my_wallet", "password");
const wallet = await wm.createWallet("./my_wallet", "password");

console.log(await wallet.address());

@@ -41,7 +41,7 @@ There are at least two ways to do so:
loadMoneroDylib(lib);

const wm = await WalletManager.new();
const wallet = await Wallet.create(wm, "./my_wallet", "password");
const wallet = await wm.createWallet("./my_wallet", "password");

console.log(await wallet.address());

10 changes: 5 additions & 5 deletions impls/monero.ts/checksum.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { moneroChecksum } from "./checksum_monero.ts";
import { getSymbol, readCString } from "./src/utils.ts";
import { dylib, loadMoneroDylib } from "./src/bindings.ts";
import { readCString } from "./src/utils.ts";
import { fns, loadMoneroDylib } from "./src/bindings.ts";

loadMoneroDylib();

@@ -21,7 +21,7 @@ export class ChecksumError extends Error {
* @returns {ChecksumError} which contains information about why checksum failed
*/
export async function validateChecksum(): Promise<ChecksumError | null> {
const cppHeaderHash = await readCString(await getSymbol("checksum_wallet2_api_c_h")!(), false);
const cppHeaderHash = await readCString(await fns.checksum_wallet2_api_c_h!(), false);
const tsHeaderHash = moneroChecksum.wallet2_api_c_h_sha256;

const errors: string[] = [];
@@ -32,14 +32,14 @@ export async function validateChecksum(): Promise<ChecksumError | null> {
errorCode++;
}

const cppSourceHash = await readCString(await getSymbol("checksum_wallet2_api_c_cpp")!(), false);
const cppSourceHash = await readCString(await fns.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 getSymbol("checksum_wallet2_api_c_exp")!(), false);
const cppExportHash = await readCString(await fns.checksum_wallet2_api_c_exp!(), false);
const tsExportHash = moneroChecksum.wallet2_api_c_exp_sha256;
if (cppExportHash !== tsExportHash) {
if (Deno.build.os !== "darwin") {
5 changes: 5 additions & 0 deletions impls/monero.ts/mod.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
export * from "./src/bindings.ts";
export * from "./src/coins.ts";
export * from "./src/coins_info.ts";
export * from "./src/pending_transaction.ts";
export * from "./src/symbols.ts";
export * from "./src/transaction_history.ts";
export * from "./src/transaction_info.ts";
export * from "./src/unsigned_transaction.ts";
export * from "./src/utils.ts";
export * from "./src/wallet.ts";
export * from "./src/wallet_manager.ts";
21 changes: 18 additions & 3 deletions impls/monero.ts/src/bindings.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { type Dylib, moneroSymbols, type MoneroTsDylib, wowneroSymbols, type WowneroTsDylib } from "./symbols.ts";
import { type MoneroSymbols, moneroSymbols, type SymbolName, type WowneroSymbols, wowneroSymbols } from "./symbols.ts";

export type MoneroDylib = Deno.DynamicLibrary<MoneroSymbols>;
export type WowneroDylib = Deno.DynamicLibrary<WowneroSymbols>;
export type Dylib = MoneroDylib | WowneroDylib;

export let dylib: Dylib;

export function loadMoneroDylib(newDylib?: MoneroTsDylib) {
let dylibPrefix = "MONERO";
export const fns = new Proxy({} as { [K in SymbolName]: MoneroDylib["symbols"][`MONERO_${K}`] }, {
get(_, symbolName: SymbolName) {
return dylib.symbols[`${dylibPrefix}_${symbolName}` as keyof Dylib["symbols"]];
},
});

export function loadMoneroDylib(newDylib?: MoneroDylib) {
dylibPrefix = "MONERO";

if (newDylib) {
dylib = newDylib;
return;
@@ -27,7 +40,9 @@ export function loadMoneroDylib(newDylib?: MoneroTsDylib) {
dylib = Deno.dlopen(libPath, moneroSymbols);
}

export function loadWowneroDylib(newDylib?: WowneroTsDylib) {
export function loadWowneroDylib(newDylib?: WowneroDylib) {
dylibPrefix = "WOWNERO";

if (newDylib) {
dylib = newDylib;
return;
53 changes: 53 additions & 0 deletions impls/monero.ts/src/coins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { CoinsInfo, type CoinsInfoPtr } from "./coins_info.ts";
import { fns } from "./bindings.ts";

export type CoinsPtr = Deno.PointerObject<"coins">;

export class Coins {
#ptr: CoinsPtr;

#coins: CoinsInfo[] = [];

constructor(ptr: CoinsPtr) {
this.#ptr = ptr;
}

async count(): Promise<number> {
return await fns.Coins_count(this.#ptr);
}

async coin(index: number): Promise<CoinsInfo | null> {
if (this.#coins[index]) {
return this.#coins[index];
}

const coinPtr = await fns.Coins_coin(this.#ptr, index);
if (!coinPtr) return null;

return CoinsInfo.new(coinPtr as CoinsInfoPtr);
}

async setFrozen(index: number) {
return await fns.Coins_setFrozen(this.#ptr, index);
}

async thaw(index: number) {
return await fns.Coins_thaw(this.#ptr, index);
}

async getAllSize(): Promise<number> {
return await fns.Coins_getAll_size(this.#ptr);
}

async getAllByIndex(index: number): Promise<unknown> {
return await fns.Coins_getAll_byIndex(this.#ptr, index);
}

async refresh(): Promise<void> {
await fns.Coins_refresh(this.#ptr);

for (const coin of this.#coins) {
coin.refresh();
}
}
}
85 changes: 85 additions & 0 deletions impls/monero.ts/src/coins_info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { fns } from "./bindings.ts";
import { readCString } from "./utils.ts";

export type CoinsInfoPtr = Deno.PointerObject<"coinsInfo">;

export class CoinsInfo {
#ptr: CoinsInfoPtr;

#hash!: string | null;
#keyImage!: string | null;
#blockHeight!: bigint;
#amount!: bigint;
#spent!: boolean;
#spentHeight!: bigint;
#frozen!: boolean;
#unlocked!: boolean;

constructor(ptr: CoinsInfoPtr) {
this.#ptr = ptr;
}

getPointer(): CoinsInfoPtr {
return this.#ptr;
}

static async new(ptr: CoinsInfoPtr): Promise<CoinsInfo> {
const instance = new CoinsInfo(ptr);
await instance.refresh();
return instance;
}

async refresh() {
const [hash, keyImage, blockHeight, amount, spent, spentHeight, frozen, unlocked] = await Promise.all([
fns.CoinsInfo_hash(this.#ptr).then(readCString),
fns.CoinsInfo_keyImage(this.#ptr).then(readCString),
fns.CoinsInfo_blockHeight(this.#ptr),
fns.CoinsInfo_amount(this.#ptr),
fns.CoinsInfo_spent(this.#ptr),
fns.CoinsInfo_spentHeight(this.#ptr),
fns.CoinsInfo_frozen(this.#ptr),
fns.CoinsInfo_unlocked(this.#ptr),
]);

this.#hash = hash;
this.#keyImage = keyImage;
this.#blockHeight = blockHeight;
this.#amount = amount;
this.#spent = spent;
this.#spentHeight = spentHeight;
this.#frozen = frozen;
this.#unlocked = unlocked;
}

get hash(): string | null {
return this.#hash;
}

get keyImage(): string | null {
return this.#keyImage;
}

get blockHeight(): bigint {
return this.#blockHeight;
}

get amount(): bigint {
return this.#amount;
}

get spent(): boolean {
return this.#spent;
}

get spentHeight(): bigint {
return this.#spentHeight;
}

get frozen(): boolean {
return this.#frozen;
}

get unlocked(): boolean {
return this.#unlocked;
}
}
117 changes: 57 additions & 60 deletions impls/monero.ts/src/pending_transaction.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,84 @@
import { CString, getSymbol, readCString, type Sanitizer } from "./utils.ts";
import { fns } from "./bindings.ts";
import { C_SEPARATOR, CString, maybeMultipleStrings, readCString } from "./utils.ts";

export type PendingTransactionPtr = Deno.PointerObject<"transactionInfo">;
export type PendingTransactionPtr = Deno.PointerObject<"pendingTransaction">;

export class PendingTransaction {
#pendingTxPtr: PendingTransactionPtr;
sanitizer?: Sanitizer;
export class PendingTransaction<MultDest extends boolean = false> {
#ptr: PendingTransactionPtr;

constructor(pendingTxPtr: PendingTransactionPtr, sanitizer?: Sanitizer) {
this.sanitizer = sanitizer;
this.#pendingTxPtr = pendingTxPtr;
}
#amount!: bigint;
#dust!: bigint;
#fee!: bigint;
#txid!: string | string[] | null;
#txCount!: bigint;

async status(): Promise<number> {
return await getSymbol("PendingTransaction_status")(this.#pendingTxPtr);
constructor(ptr: PendingTransactionPtr) {
this.#ptr = ptr;
}

async errorString(): Promise<string | null> {
if (!await this.status()) return null;
static async new(ptr: PendingTransactionPtr): Promise<PendingTransaction> {
const instance = new PendingTransaction(ptr);

const error = await getSymbol("PendingTransaction_errorString")(this.#pendingTxPtr);
if (!error) return null;
const [amount, dust, fee, txCount, txid] = await Promise.all([
fns.PendingTransaction_amount(ptr),
fns.PendingTransaction_dust(ptr),
fns.PendingTransaction_fee(ptr),
fns.PendingTransaction_txCount(ptr),
fns.PendingTransaction_txid(ptr, C_SEPARATOR),
]);

return await readCString(error) || null;
}
instance.#amount = amount;
instance.#dust = dust;
instance.#fee = fee;
instance.#txCount = txCount;
instance.#txid = maybeMultipleStrings(await readCString(txid));

async throwIfError(sanitize = true): Promise<void> {
const maybeError = await this.errorString();
if (maybeError) {
if (sanitize) this.sanitizer?.();
throw new Error(maybeError);
}
return instance;
}

async commit(fileName: string, overwrite: boolean, sanitize = true): Promise<boolean> {
const bool = await getSymbol("PendingTransaction_commit")(
this.#pendingTxPtr,
CString(fileName),
overwrite,
);
await this.throwIfError(sanitize);
return bool;
get amount(): bigint {
return this.#amount;
}

async commitUR(maxFragmentLength: number): Promise<string | null> {
const commitUR = getSymbol("PendingTransaction_commitUR");

if (!commitUR) {
return null;
}
get dust(): bigint {
return this.#dust;
}

const result = await commitUR(
this.#pendingTxPtr,
maxFragmentLength,
);
get fee(): bigint {
return this.#fee;
}

if (!result) return null;
await this.throwIfError();
return await readCString(result) || null;
get txCount(): bigint {
return this.#txCount;
}

async amount(): Promise<bigint> {
return await getSymbol("PendingTransaction_amount")(this.#pendingTxPtr);
async commit(fileName: string, overwrite: boolean): Promise<boolean> {
return await fns.PendingTransaction_commit(this.#ptr, CString(fileName), overwrite);
}

async dust(): Promise<bigint> {
return await getSymbol("PendingTransaction_dust")(this.#pendingTxPtr);
async commitUR(maxFragmentLength: number): Promise<string | null> {
const commitUR = fns.PendingTransaction_commitUR;
if (!commitUR) return null;

return await readCString(
await commitUR(this.#ptr, maxFragmentLength),
);
}

async fee(): Promise<bigint> {
return await getSymbol("PendingTransaction_fee")(this.#pendingTxPtr);
async status(): Promise<number> {
return await fns.PendingTransaction_status(this.#ptr);
}

async txid(separator: string, sanitize = true): Promise<string | null> {
const result = await getSymbol("PendingTransaction_txid")(
this.#pendingTxPtr,
CString(separator),
);
if (!result) return null;
await this.throwIfError(sanitize);
return await readCString(result) || null;
async errorString(): Promise<string | null> {
if (!await this.status()) return null;
const error = await fns.PendingTransaction_errorString(this.#ptr);
return await readCString(error);
}

async txCount(): Promise<bigint> {
return await getSymbol("PendingTransaction_txCount")(this.#pendingTxPtr);
async throwIfError(): Promise<void> {
const maybeError = await this.errorString();
if (maybeError) {
throw new Error(maybeError);
}
}
}
2,610 changes: 2,257 additions & 353 deletions impls/monero.ts/src/symbols.ts

Large diffs are not rendered by default.

33 changes: 20 additions & 13 deletions impls/monero.ts/src/transaction_history.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
import { fns } from "./bindings.ts";
import { TransactionInfo, TransactionInfoPtr } from "./transaction_info.ts";
import { CString, getSymbol } from "./utils.ts";
import { CString } from "./utils.ts";

export type TransactionHistoryPtr = Deno.PointerObject<"transactionHistory">;

export class TransactionHistory {
#txHistoryPtr: TransactionHistoryPtr;
#ptr: TransactionHistoryPtr;

constructor(txHistoryPtr: TransactionHistoryPtr) {
this.#txHistoryPtr = txHistoryPtr;
#count!: number;

constructor(ptr: TransactionHistoryPtr) {
this.#ptr = ptr;
}

static async new(ptr: TransactionHistoryPtr) {
const instance = new TransactionHistory(ptr);
instance.#count = await fns.TransactionHistory_count(ptr);
return instance;
}

async count(): Promise<number> {
return await getSymbol("TransactionHistory_count")(this.#txHistoryPtr);
get count(): number {
return this.#count;
}

async transaction(index: number): Promise<TransactionInfo> {
return new TransactionInfo(
(
await getSymbol("TransactionHistory_transaction")(this.#txHistoryPtr, index)
) as TransactionInfoPtr,
return TransactionInfo.new(
await fns.TransactionHistory_transaction(this.#ptr, index) as TransactionInfoPtr,
);
}

async refresh(): Promise<void> {
await getSymbol("TransactionHistory_refresh")(this.#txHistoryPtr);
await fns.TransactionHistory_refresh(this.#ptr);
}

async setTxNote(transactionId: string, note: string): Promise<void> {
await getSymbol("TransactionHistory_setTxNote")(
this.#txHistoryPtr,
await fns.TransactionHistory_setTxNote(
this.#ptr,
CString(transactionId),
CString(note),
);
169 changes: 106 additions & 63 deletions impls/monero.ts/src/transaction_info.ts
Original file line number Diff line number Diff line change
@@ -1,104 +1,147 @@
import { dylib } from "./bindings.ts";
import { getSymbol, readCString, Sanitizer } from "./utils.ts";
import { fns } from "./bindings.ts";
import { C_SEPARATOR, CString, maybeMultipleStrings, readCString, SEPARATOR } from "./utils.ts";

export type TransactionInfoPtr = Deno.PointerObject<"transactionInfo">;
export type TransactionInfoPtr = Deno.PointerObject<"pendingTransaction">;

export class TransactionInfo {
#txInfoPtr: TransactionInfoPtr;
sanitizer?: Sanitizer;

constructor(txInfoPtr: TransactionInfoPtr, sanitizer?: Sanitizer) {
this.#txInfoPtr = txInfoPtr;
this.sanitizer = sanitizer;
}
export interface TransferData {
address: string | null;
amount: bigint;
}

async direction(): Promise<"in" | "out"> {
switch (await getSymbol("TransactionInfo_direction")(this.#txInfoPtr)) {
case 0:
return "in";
case 1:
return "out";
default:
await this.sanitizer?.();
throw new Error("Invalid TransactionInfo direction");
export class TransactionInfo<MultDest extends boolean = boolean> {
#ptr: TransactionInfoPtr;

#amount!: bigint;
#fee!: bigint;
#timestamp!: bigint;
#transfersCount!: number;
#paymentId!: string | null;
#hash!: string | null;

#subaddrAccount!: number;
#subaddrIndex!: string | null;

#transfers!: readonly TransferData[];

constructor(ptr: TransactionInfoPtr) {
this.#ptr = ptr;
}

static async new(ptr: TransactionInfoPtr): Promise<TransactionInfo> {
const instance = new TransactionInfo(ptr);

const [amount, paymentId, fee, hash, subaddrIndex, subaddrAccount, timestamp, transfersCount] = await Promise.all([
fns.TransactionInfo_amount(ptr),
fns.TransactionInfo_paymentId(ptr).then(readCString),
fns.TransactionInfo_fee(ptr),
fns.TransactionInfo_hash(ptr).then(readCString),
fns.TransactionInfo_subaddrIndex(ptr, C_SEPARATOR).then(readCString),
fns.TransactionInfo_subaddrAccount(ptr),
fns.TransactionInfo_timestamp(ptr),
fns.TransactionInfo_transfers_count(ptr),
]);

instance.#amount = amount;
instance.#fee = fee;
instance.#timestamp = timestamp;
instance.#transfersCount = transfersCount;
instance.#paymentId = paymentId;
instance.#hash = hash;

instance.#subaddrAccount = subaddrAccount;
instance.#subaddrIndex = subaddrIndex;

const transfers = [];
for (let i = 0; i < transfersCount; ++i) {
const [amount, address] = await Promise.all([
fns.TransactionInfo_transfers_amount(ptr, i),
fns.TransactionInfo_transfers_address(ptr, i).then(readCString),
]);

transfers.push({ amount, address });
}
}
Object.freeze(transfers);
instance.#transfers = transfers;

async isPending(): Promise<boolean> {
return await getSymbol("TransactionInfo_isPending")(this.#txInfoPtr);
return instance;
}

async isFailed(): Promise<boolean> {
return await getSymbol("TransactionInfo_isFailed")(this.#txInfoPtr);
get amount(): bigint {
return this.#amount;
}

async isCoinbase(): Promise<boolean> {
return await getSymbol("TransactionInfo_isCoinbase")(this.#txInfoPtr);
get fee(): bigint {
return this.#fee;
}

async amount(): Promise<bigint> {
return await getSymbol("TransactionInfo_amount")(this.#txInfoPtr);
get timestamp(): bigint {
return this.#timestamp;
}

async fee(): Promise<bigint> {
return await getSymbol("TransactionInfo_fee")(this.#txInfoPtr);
get transfersCount(): number {
return this.#transfersCount;
}

async blockHeight(): Promise<bigint> {
return await getSymbol("TransactionInfo_blockHeight")(this.#txInfoPtr);
get paymentId(): string | null {
return this.#paymentId;
}

async description(): Promise<string> {
const description = await getSymbol("TransactionInfo_description")(this.#txInfoPtr);
return await readCString(description) || "";
get hash(): string | null {
return this.#hash;
}

async subaddrIndex(): Promise<string> {
const subaddrIndex = await getSymbol("TransactionInfo_subaddrIndex")(this.#txInfoPtr);
return await readCString(subaddrIndex) || "";
get subaddrAccount(): number {
return this.#subaddrAccount;
}

async subaddrAccount(): Promise<number> {
return await getSymbol("TransactionInfo_subaddrAccount")(this.#txInfoPtr);
get subaddrIndex(): string | null {
return this.#subaddrIndex;
}

async label(): Promise<string> {
const label = await getSymbol("TransactionInfo_label")(this.#txInfoPtr);
return await readCString(label) || "";
get transfers(): readonly TransferData[] {
return this.#transfers;
}

async confirmations(): Promise<bigint> {
return await getSymbol("TransactionInfo_confirmations")(this.#txInfoPtr);
async direction(): Promise<"in" | "out"> {
switch (await fns.TransactionInfo_direction(this.#ptr)) {
case 0:
return "in";
case 1:
return "out";
default:
throw new Error("Invalid TransactionInfo direction");
}
}

async unlockTime(): Promise<bigint> {
return await getSymbol("TransactionInfo_unlockTime")(this.#txInfoPtr);
async description(): Promise<string | null> {
return await readCString(
await fns.TransactionInfo_description(this.#ptr),
);
}

async hash(): Promise<string> {
const hash = await getSymbol("TransactionInfo_hash")(this.#txInfoPtr);
return await readCString(hash) || "";
async label(): Promise<string | null> {
return await readCString(
await fns.TransactionInfo_label(this.#ptr),
);
}

async timestamp(): Promise<bigint> {
return await getSymbol("TransactionInfo_timestamp")(this.#txInfoPtr);
async confirmations(): Promise<bigint> {
return await fns.TransactionInfo_confirmations(this.#ptr);
}

async paymentId(): Promise<string> {
const paymentId = await getSymbol("TransactionInfo_paymentId")(this.#txInfoPtr);
return await readCString(paymentId) || "";
async unlockTime(): Promise<bigint> {
return await fns.TransactionInfo_unlockTime(this.#ptr);
}

async transfersCount(): Promise<number> {
return await getSymbol("TransactionInfo_transfers_count")(this.#txInfoPtr);
async isPending(): Promise<boolean> {
return await fns.TransactionInfo_isPending(this.#ptr);
}

async transfersAmount(index: number): Promise<bigint> {
return await getSymbol("TransactionInfo_transfers_amount")(this.#txInfoPtr, index);
async isFailed(): Promise<boolean> {
return await fns.TransactionInfo_isFailed(this.#ptr);
}

async transfersAddress(index: number): Promise<string> {
const transfersAddress = await getSymbol("TransactionInfo_transfers_address")(this.#txInfoPtr, index);
return await readCString(transfersAddress) || "";
async isCoinbase(): Promise<boolean> {
return await fns.TransactionInfo_isCoinbase(this.#ptr);
}
}
79 changes: 79 additions & 0 deletions impls/monero.ts/src/unsigned_transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { fns } from "./bindings.ts";
import { C_SEPARATOR, CString, maybeMultipleStrings, readCString } from "./utils.ts";

export type UnsignedTransactionPtr = Deno.PointerObject<"pendingTransaction">;

export class UnsignedTransaction<MultDest extends boolean = false> {
#ptr: UnsignedTransactionPtr;

#amount!: string | string[] | null;
#fee!: string | string[] | null;
#txCount!: bigint;
#paymentId!: string | null;
#recipientAddress!: string | string[] | null;

constructor(ptr: UnsignedTransactionPtr) {
this.#ptr = ptr;
}

async status(): Promise<number> {
return await fns.UnsignedTransaction_status(this.#ptr);
}

async errorString(): Promise<string | null> {
return await readCString(await fns.UnsignedTransaction_errorString(this.#ptr));
}

static async new(ptr: UnsignedTransactionPtr): Promise<UnsignedTransaction> {
const instance = new UnsignedTransaction(ptr);

const [amount, paymentId, fee, txCount, recipientAddress] = await Promise.all([
fns.UnsignedTransaction_amount(ptr, C_SEPARATOR).then(readCString),
fns.UnsignedTransaction_paymentId(ptr, C_SEPARATOR).then(readCString),
fns.UnsignedTransaction_fee(ptr, C_SEPARATOR).then(readCString),
fns.UnsignedTransaction_txCount(ptr),
fns.UnsignedTransaction_recipientAddress(ptr, C_SEPARATOR).then(readCString),
]);

instance.#amount = maybeMultipleStrings(amount);
instance.#fee = maybeMultipleStrings(fee);
instance.#recipientAddress = maybeMultipleStrings(recipientAddress);
instance.#txCount = txCount;
instance.#paymentId = paymentId;

return instance;
}

get amount(): string | string[] | null {
return this.#amount;
}

get fee(): string | string[] | null {
return this.#fee;
}

get txCount(): bigint {
return this.#txCount;
}

get paymentId(): string | null {
return this.#paymentId;
}

get recipientAddress(): string | string[] | null {
return this.#recipientAddress;
}

async sign(signedFileName: string): Promise<boolean> {
return await fns.UnsignedTransaction_sign(this.#ptr, CString(signedFileName));
}

async signUR(maxFragmentLength: number): Promise<string | null> {
const signUR = fns.UnsignedTransaction_signUR;
if (!signUR) return null;

return await readCString(
await signUR(this.#ptr, maxFragmentLength),
);
}
}
32 changes: 14 additions & 18 deletions impls/monero.ts/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { dylib } from "../mod.ts";
import type { moneroSymbols, MoneroTsDylib, WowneroTsDylib } from "./symbols.ts";

export type Sanitizer = () => void | PromiseLike<void>;
import { fns } from "./bindings.ts";

const textEncoder = new TextEncoder();
export function CString(string: string): Deno.PointerValue<string> {
return Deno.UnsafePointer.of(textEncoder.encode(`${string}\x00`));
export const SEPARATOR = ",";
export const C_SEPARATOR = CString(SEPARATOR);

export function maybeMultipleStrings(input: string): string | string[];
export function maybeMultipleStrings(input: null | string): null | string | string[];
export function maybeMultipleStrings(input: null | string): null | string | string[] {
if (!input) return null;
const multiple = input.split(SEPARATOR);
return multiple.length === 1 ? multiple[0] : multiple;
}

type SymbolWithoutPrefix = keyof typeof moneroSymbols extends `MONERO_${infer DylibSymbol}` ? DylibSymbol : never;
export function getSymbol<S extends SymbolWithoutPrefix>(
symbol: S,
): MoneroTsDylib["symbols"][`MONERO_${S}`] | WowneroTsDylib["symbols"][`WOWNERO_${S}`] {
if ("MONERO_free" in dylib.symbols) {
return dylib.symbols[`MONERO_${symbol}` as const];
} else {
return dylib.symbols[`WOWNERO_${symbol}` as const];
}
export function CString(string: string): Deno.PointerValue<string> {
return Deno.UnsafePointer.of(textEncoder.encode(`${string}\x00`));
}

/**
@@ -29,9 +26,8 @@ export async function readCString(pointer: Deno.PointerObject, free?: boolean):
export async function readCString(pointer: Deno.PointerValue, free?: boolean): Promise<string | null>;
export async function readCString(pointer: Deno.PointerValue, free = true): Promise<string | null> {
if (!pointer) return null;

const string = new Deno.UnsafePointerView(pointer).getCString();
if (free) {
await getSymbol("free")(pointer);
}
if (string && free) await fns.free(pointer);
return string;
}
446 changes: 234 additions & 212 deletions impls/monero.ts/src/wallet.ts

Large diffs are not rendered by default.

138 changes: 130 additions & 8 deletions impls/monero.ts/src/wallet_manager.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,148 @@
import { getSymbol, Sanitizer } from "./utils.ts";
import { fns } from "./bindings.ts";
import { CString } from "./utils.ts";
import { Wallet, WalletPtr } from "./wallet.ts";

export type WalletManagerPtr = Deno.PointerObject<"walletManager">;

export class WalletManager {
#ptr: WalletManagerPtr;
sanitizer?: Sanitizer;

constructor(walletManagerPtr: WalletManagerPtr, sanitizer?: Sanitizer) {
constructor(walletManagerPtr: WalletManagerPtr) {
this.#ptr = walletManagerPtr;
this.sanitizer = sanitizer;
}

getPointer(): WalletManagerPtr {
return this.#ptr;
}

static async new(sanitizer?: Sanitizer) {
const ptr = await getSymbol("WalletManagerFactory_getWalletManager")();
static async new() {
const ptr = await fns.WalletManagerFactory_getWalletManager();
if (!ptr) {
sanitizer?.();
throw new Error("Failed retrieving wallet manager");
}
return new WalletManager(ptr as WalletManagerPtr, sanitizer);

return new WalletManager(ptr as WalletManagerPtr);
}

async setDaemonAddress(address: string): Promise<void> {
return await fns.WalletManager_setDaemonAddress(this.#ptr, CString(address));
}

async createWallet(path: string, password: string): Promise<Wallet> {
const walletPtr = await fns.WalletManager_createWallet(
this.#ptr,
CString(path),
CString(password),
CString("English"),
0,
);

const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}

async openWallet(path: string, password: string): Promise<Wallet> {
const walletPtr = await fns.WalletManager_openWallet(
this.#ptr,
CString(path),
CString(password),
0,
);

const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}

async recoverWallet(
path: string,
password: string,
mnemonic: string,
restoreHeight: bigint,
seedOffset: string = "",
): Promise<Wallet> {
const walletPtr = await fns.WalletManager_recoveryWallet(
this.#ptr,
CString(path),
CString(password),
CString(mnemonic),
0,
restoreHeight,
1n,
CString(seedOffset),
);

const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}

async recoverFromPolyseed(
path: string,
password: string,
mnemonic: string,
restoreHeight: bigint,
passphrase = "",
): Promise<Wallet> {
return await this.createFromPolyseed(
path,
password,
mnemonic,
restoreHeight,
passphrase,
false,
);
}

async createFromPolyseed(
path: string,
password: string,
mnemonic: string,
restoreHeight: bigint,
passphrase = "",
newWallet = true,
): Promise<Wallet> {
const walletPtr = await fns.WalletManager_createWalletFromPolyseed(
this.#ptr,
CString(path),
CString(password),
0,
CString(mnemonic),
CString(passphrase),
newWallet,
restoreHeight,
1n,
);

const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}

async recoverFromKeys(
path: string,
password: string,
restoreHeight: bigint,
address: string,
viewKey: string,
spendKey: string,
): Promise<Wallet> {
const walletPtr = await fns.WalletManager_createWalletFromKeys(
this.#ptr,
CString(path),
CString(password),
CString("English"),
0,
restoreHeight,
CString(address),
CString(viewKey),
CString(spendKey),
0n,
);

const wallet = new Wallet(this, walletPtr as WalletPtr);
await wallet.throwIfError();
return wallet;
}
}
76 changes: 15 additions & 61 deletions tests/compare.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,34 @@
import { moneroSymbols as symbols, type MoneroTsDylib, type WowneroTsDylib } from "../impls/monero.ts/src/symbols.ts";
import { loadMoneroDylib, loadWowneroDylib } from "../impls/monero.ts/src/bindings.ts";
import { Wallet, WalletManager } from "../impls/monero.ts/mod.ts";
import { readCString } from "../impls/monero.ts/src/utils.ts";
import { assertEquals } from "jsr:@std/assert";

import {
loadMoneroDylib,
loadWowneroDylib,
moneroSymbols,
WalletManager,
wowneroSymbols,
} from "../impls/monero.ts/mod.ts";

const coin = Deno.args[0] as "monero" | "wownero";
const version = Deno.args[1];
const walletInfo = JSON.parse(Deno.args[2]);

const moneroSymbols = {
...symbols,

"MONERO_Wallet_secretViewKey": {
nonblocking: true,
// void* wallet_ptr
parameters: ["pointer"],
// const char*
result: "pointer",
},
"MONERO_Wallet_publicViewKey": {
nonblocking: true,
// void* wallet_ptr
parameters: ["pointer"],
// const char*
result: "pointer",
},

"MONERO_Wallet_secretSpendKey": {
nonblocking: true,
// void* wallet_ptr
parameters: ["pointer"],
// const char*
result: "pointer",
},
"MONERO_Wallet_publicSpendKey": {
nonblocking: true,
// void* wallet_ptr
parameters: ["pointer"],
// const char*
result: "pointer",
},
} as const;

type ReplaceMonero<T extends string> = T extends `MONERO${infer Y}` ? `WOWNERO${Y}` : never;
type WowneroSymbols = { [Key in keyof typeof moneroSymbols as ReplaceMonero<Key>]: (typeof moneroSymbols)[Key] };
const wowneroSymbols = Object.fromEntries(
Object.entries(moneroSymbols).map(([key, value]) => [key.replace("MONERO", "WOWNERO"), value]),
) as WowneroSymbols;

let getKey: (wallet: Wallet, type: `${"secret" | "public"}${"Spend" | "View"}Key`) => Promise<string | null>;

if (coin === "monero") {
const dylib = Deno.dlopen(`tests/libs/${version}/monero_libwallet2_api_c.so`, moneroSymbols);
loadMoneroDylib(dylib as MoneroTsDylib);

getKey = async (wallet, type) =>
await readCString(await dylib.symbols[`MONERO_Wallet_${type}` as const](wallet.getPointer()));
loadMoneroDylib(dylib);
} else {
const dylib = Deno.dlopen(`tests/libs/${version}/wownero_libwallet2_api_c.so`, wowneroSymbols);
loadWowneroDylib(dylib as WowneroTsDylib);

getKey = async (wallet, type) =>
await readCString(
await dylib.symbols[`WOWNERO_Wallet_${type}` as const](wallet.getPointer()),
);
loadWowneroDylib(dylib);
}

const walletManager = await WalletManager.new();
const wallet = await Wallet.open(walletManager, walletInfo.path, walletInfo.password);
const wallet = await walletManager.openWallet(walletInfo.path, walletInfo.password);

assertEquals(await wallet.address(), walletInfo.address);

assertEquals(await getKey(wallet, "publicSpendKey"), walletInfo.publicSpendKey);
assertEquals(await getKey(wallet, "secretSpendKey"), walletInfo.secretSpendKey);
assertEquals(await wallet.publicSpendKey(), walletInfo.publicSpendKey);
assertEquals(await wallet.secretSpendKey(), walletInfo.secretSpendKey);

assertEquals(await getKey(wallet, "publicViewKey"), walletInfo.publicViewKey);
assertEquals(await getKey(wallet, "secretViewKey"), walletInfo.secretViewKey);
assertEquals(await wallet.publicViewKey(), walletInfo.publicViewKey);
assertEquals(await wallet.secretViewKey(), walletInfo.secretViewKey);

await wallet.store(walletInfo.path);
4 changes: 4 additions & 0 deletions tests/deno.lock
100755 → 100644
632 changes: 632 additions & 0 deletions tests/integration.test.ts

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions tests/utils.ts
Original file line number Diff line number Diff line change
@@ -14,9 +14,9 @@ export async function downloadMoneroCli() {
const MONERO_CLI_FILE_NAME = "monero-linux-x64-v0.18.3.4";
const MONERO_WALLET_CLI_URL = `https://downloads.getmonero.org/cli/${MONERO_CLI_FILE_NAME}.tar.bz2`;

await $`wget ${MONERO_WALLET_CLI_URL}`;
await $`wget -q -o /dev/null ${MONERO_WALLET_CLI_URL}`;
await $
.raw`tar -xvf ${MONERO_CLI_FILE_NAME}.tar.bz2 --one-top-level=monero-cli --strip-components=1 -C tests`;
.raw`tar -xf ${MONERO_CLI_FILE_NAME}.tar.bz2 --one-top-level=monero-cli --strip-components=1 -C tests`;
await $.raw`rm ${MONERO_CLI_FILE_NAME}.tar.bz2`;
}

@@ -25,9 +25,9 @@ export async function downloadWowneroCli() {
const WOWNERO_WALLET_CLI_URL =
`https://codeberg.org/wownero/wownero/releases/download/v0.11.2.0/wownero-x86_64-linux-gnu-59db3fe8d.tar.bz2`;

await $`wget ${WOWNERO_WALLET_CLI_URL}`;
await $`wget -q -o /dev/null ${WOWNERO_WALLET_CLI_URL}`;
await $
.raw`tar -xvf ${WOWNERO_CLI_FILE_NAME}.tar.bz2 --one-top-level=wownero-cli --strip-components=1 -C tests`;
.raw`tar -xf ${WOWNERO_CLI_FILE_NAME}.tar.bz2 --one-top-level=wownero-cli --strip-components=1 -C tests`;
await $.raw`rm ${WOWNERO_CLI_FILE_NAME}.tar.bz2`;
}

0 comments on commit 8f52aeb

Please sign in to comment.