Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ export type { SubmitResult, TransactionStatus } from './types/transaction';

// 6. Standalone functions
export { createEscrowAccount, lockCustodyFunds, anchorTrustHash, verifyEventHash } from './escrow';
export { buildMultisigTransaction } from './transactions';
export { buildMultisigTransaction, buildPaymentOp, decodeMemo } from './transactions';
export type { DecodedMemo } from './transactions';
export { getMinimumReserve } from './accounts';
71 changes: 71 additions & 0 deletions src/transactions/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Memo, MemoType } from '@stellar/stellar-sdk';

export type DecodedMemo =
| { type: 'MEMO_NONE'; value: null }
| { type: 'MEMO_TEXT'; value: string }
| { type: 'MEMO_ID'; value: string }
| { type: 'MEMO_HASH'; value: string }
| { type: 'MEMO_RETURN'; value: string };

/**
* decodeMemo
*
* Decodes a Stellar Memo object back to its original value where possible.
*
* - MEMO_NONE → null
* - MEMO_TEXT → original string
* - MEMO_ID → numeric value as string
* - MEMO_HASH → hex digest string (cannot be reversed — returned as-is)
* - MEMO_RETURN → hex digest string (cannot be reversed — returned as-is)
*
* @param memo - A Stellar SDK Memo object
* @returns DecodedMemo object with type and value
*
* @example
* decodeMemo(Memo.none())
* // { type: 'MEMO_NONE', value: null }
*
* decodeMemo(Memo.text('hello'))
* // { type: 'MEMO_TEXT', value: 'hello' }
*
* decodeMemo(Memo.id('12345'))
* // { type: 'MEMO_ID', value: '12345' }
*
* decodeMemo(Memo.hash(Buffer.alloc(32)))
* // { type: 'MEMO_HASH', value: '0000...0000' }
*/
export function decodeMemo(memo: Memo): DecodedMemo {
switch (memo.type as MemoType) {
case 'none':
return { type: 'MEMO_NONE', value: null };

case 'text':
return { type: 'MEMO_TEXT', value: memo.value as string };

case 'id':
return { type: 'MEMO_ID', value: String(memo.value) };

case 'hash': {
const hashValue = memo.value as Buffer;
return {
type: 'MEMO_HASH',
value: Buffer.isBuffer(hashValue)
? hashValue.toString('hex')
: String(hashValue),
};
}

case 'return': {
const returnValue = memo.value as Buffer;
return {
type: 'MEMO_RETURN',
value: Buffer.isBuffer(returnValue)
? returnValue.toString('hex')
: String(returnValue),
};
}

default:
return { type: 'MEMO_NONE', value: null };
}
}
24 changes: 24 additions & 0 deletions src/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,26 @@
import { Asset, Operation } from '@stellar/stellar-sdk';
import { isValidPublicKey, isValidAmount } from '../utils/validation';
import { ValidationError } from '../utils/errors';

export { decodeMemo, type DecodedMemo } from './builder';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function buildMultisigTransaction(..._args: unknown[]): unknown { return undefined; }

export function buildPaymentOp({
destination,
amount,
asset = Asset.native(),
}: {
destination: string;
amount: string;
asset?: Asset;
}): ReturnType<typeof Operation.payment> {
if (!isValidPublicKey(destination)) {
throw new ValidationError('destination', `Invalid Stellar public key: ${destination}`);
}
if (!isValidAmount(amount)) {
throw new ValidationError('amount', `Invalid amount: ${amount}. Must be a positive decimal string with up to 7 decimal places.`);
}
return Operation.payment({ destination, asset, amount });
}
47 changes: 47 additions & 0 deletions tests/unit/transactions/decodeMemo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Memo } from '@stellar/stellar-sdk';
import { decodeMemo } from '../../../src/transactions/builder';

describe('decodeMemo', () => {
it('decodes MEMO_NONE to null', () => {
const result = decodeMemo(Memo.none());
expect(result.type).toBe('MEMO_NONE');
expect(result.value).toBeNull();
});

it('decodes MEMO_TEXT to original string', () => {
const result = decodeMemo(Memo.text('hello world'));
expect(result.type).toBe('MEMO_TEXT');
expect(result.value).toBe('hello world');
});

it('decodes MEMO_TEXT with empty string', () => {
const result = decodeMemo(Memo.text(''));
expect(result.type).toBe('MEMO_TEXT');
expect(result.value).toBe('');
});

it('decodes MEMO_ID to numeric string', () => {
const result = decodeMemo(Memo.id('12345'));
expect(result.type).toBe('MEMO_ID');
expect(result.value).toBe('12345');
});

it('decodes MEMO_HASH to hex string', () => {
const hashBuffer = Buffer.alloc(32, 0xab);
const result = decodeMemo(Memo.hash(hashBuffer));
expect(result.type).toBe('MEMO_HASH');
expect(result.value).toBe('ab'.repeat(32));
});

it('decodes MEMO_HASH of all zeros', () => {
const hashBuffer = Buffer.alloc(32, 0);
const result = decodeMemo(Memo.hash(hashBuffer));
expect(result.type).toBe('MEMO_HASH');
expect(result.value).toBe('00'.repeat(32));
});

it('is exported from the public index', async () => {
const mod = await import('../../../src/index');
expect(mod.decodeMemo).toBeDefined();
});
});