Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ export {
anchorTrustHash,
verifyEventHash,
} from './escrow';
export { buildMultisigTransaction } from './transactions';
export { buildMultisigTransaction, buildSetOptionsOp } from './transactions';
export { getMinimumReserve, generateKeypair } from './accounts';
77 changes: 77 additions & 0 deletions src/transactions/builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Operation, xdr } from '@stellar/stellar-sdk';

import { ValidationError } from '../utils/errors';
import { isValidPublicKey } from '../utils/validation';

export interface SetOptionsSignerInput {
publicKey: string;
weight: number;
}

export interface SetOptionsThresholdsInput {
low: number;
medium: number;
high: number;
}

export interface BuildSetOptionsOpParams {
signers?: SetOptionsSignerInput[];
thresholds?: SetOptionsThresholdsInput;
masterWeight?: number;
}

function validateUint8Field(field: string, value: number): void {
if (!Number.isInteger(value) || value < 0 || value > 255) {
throw new ValidationError(field, 'Must be an integer between 0 and 255');
}
}

export function buildSetOptionsOp(params: BuildSetOptionsOpParams): xdr.Operation[] {
const operations: xdr.Operation[] = [];

if (params.signers) {
params.signers.forEach((signer, index) => {
if (!isValidPublicKey(signer.publicKey)) {
throw new ValidationError(`signers[${index}].publicKey`, 'Invalid public key');
}

validateUint8Field(`signers[${index}].weight`, signer.weight);

operations.push(
Operation.setOptions({
signer: {
ed25519PublicKey: signer.publicKey,
weight: signer.weight,
},
}),
);
});
}

if (params.masterWeight !== undefined) {
validateUint8Field('masterWeight', params.masterWeight);
}

if (params.thresholds) {
validateUint8Field('thresholds.low', params.thresholds.low);
validateUint8Field('thresholds.medium', params.thresholds.medium);
validateUint8Field('thresholds.high', params.thresholds.high);
}

if (params.masterWeight !== undefined || params.thresholds) {
operations.push(
Operation.setOptions({
...(params.masterWeight !== undefined ? { masterWeight: params.masterWeight } : {}),
...(params.thresholds
? {
lowThreshold: params.thresholds.low,
medThreshold: params.thresholds.medium,
highThreshold: params.thresholds.high,
}
: {}),
}),
);
}

return operations;
}
1 change: 1 addition & 0 deletions src/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import { HorizonSubmitError } from '../utils/errors';
import { TESTNET_HORIZON_URL } from '../utils/constants';
export { buildSetOptionsOp } from './builder';

/**
* Internal: Fetch a single transaction status from Horizon by hash.
Expand Down
89 changes: 87 additions & 2 deletions tests/unit/transactions/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,98 @@

import { buildMultisigTransaction, fetchTransactionOnce } from '../../../src/transactions';
import { HorizonSubmitError } from '../../../src/utils/errors';
import { Keypair, Operation } from '@stellar/stellar-sdk';

import { buildMultisigTransaction, buildSetOptionsOp, fetchTransactionOnce } from '../../../src/transactions';
import { HorizonSubmitError, ValidationError } from '../../../src/utils/errors';

describe('transactions module placeholders', () => {
it('exports callable placeholder function', () => {
expect(buildMultisigTransaction()).toBeUndefined();
});
});

describe('buildSetOptionsOp', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('builds setOptions operation for adding a signer', () => {
const signerPublicKey = Keypair.random().publicKey();
const setOptionsSpy = jest.spyOn(Operation, 'setOptions');

const operations = buildSetOptionsOp({
signers: [{ publicKey: signerPublicKey, weight: 1 }],
});

expect(operations).toHaveLength(1);
expect(setOptionsSpy).toHaveBeenCalledWith({
signer: { ed25519PublicKey: signerPublicKey, weight: 1 },
});
});

it('builds setOptions operation for removing a signer with weight 0', () => {
const signerPublicKey = Keypair.random().publicKey();
const setOptionsSpy = jest.spyOn(Operation, 'setOptions');

const operations = buildSetOptionsOp({
signers: [{ publicKey: signerPublicKey, weight: 0 }],
});

expect(operations).toHaveLength(1);
expect(setOptionsSpy).toHaveBeenCalledWith({
signer: { ed25519PublicKey: signerPublicKey, weight: 0 },
});
});

it('builds setOptions operation for thresholds', () => {
const setOptionsSpy = jest.spyOn(Operation, 'setOptions');

const operations = buildSetOptionsOp({
thresholds: { low: 1, medium: 2, high: 3 },
});

expect(operations).toHaveLength(1);
expect(setOptionsSpy).toHaveBeenCalledWith({
lowThreshold: 1,
medThreshold: 2,
highThreshold: 3,
});
});

it('builds mixed setOptions operations for signers and thresholds', () => {
const signerPublicKey = Keypair.random().publicKey();
const setOptionsSpy = jest.spyOn(Operation, 'setOptions');

const operations = buildSetOptionsOp({
signers: [{ publicKey: signerPublicKey, weight: 2 }],
thresholds: { low: 1, medium: 2, high: 2 },
masterWeight: 0,
});

expect(operations).toHaveLength(2);
expect(setOptionsSpy).toHaveBeenNthCalledWith(1, {
signer: { ed25519PublicKey: signerPublicKey, weight: 2 },
});
expect(setOptionsSpy).toHaveBeenNthCalledWith(2, {
masterWeight: 0,
lowThreshold: 1,
medThreshold: 2,
highThreshold: 2,
});
});

it('throws ValidationError when a signer public key is invalid', () => {
const setOptionsSpy = jest.spyOn(Operation, 'setOptions');

expect(() =>
buildSetOptionsOp({
signers: [{ publicKey: 'INVALID_PUBLIC_KEY', weight: 1 }],
}),
).toThrow(ValidationError);

expect(setOptionsSpy).not.toHaveBeenCalled();
});
});

describe('fetchTransactionOnce', () => {
const hash = 'abc123';
const baseUrl = 'https://horizon-testnet.stellar.org/transactions/';
Expand Down
Loading