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
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Integration tests for execute functionality.
* These tests demonstrate the execute integration working against testnet contracts.
*
* NOTE: These tests are skipped by default as they require:
* - A deployed account contract on testnet
* - A funded source account
* - Network connectivity to Stellar testnet
*
* To run these tests:
* 1. Deploy an account contract to testnet
* 2. Update the CONTRACT_ID and SOURCE_ACCOUNT constants
* 3. Remove the .skip from describe.skip
*/

import { Networks } from '@stellar/stellar-sdk';
import { AccountContract } from '../account-contract';
import type { ExecuteOptions } from '../execute';

describe.skip('execute integration (testnet)', () => {
// Update these values for actual integration testing
const CONTRACT_ID = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM';
const SOURCE_ACCOUNT = 'GCKFBEIYTKP6RCZX6DSQF22OLNXY2SOGLVUQ6RGE4VW6HKPOLJZX6YTV';

let contract: AccountContract;
let executeOptions: ExecuteOptions;

beforeAll(() => {
contract = new AccountContract(CONTRACT_ID);

// Mock server for integration testing
// In real integration tests, you would use actual Soroban RPC server
const mockServer = {
getAccount: jest.fn().mockResolvedValue({
id: SOURCE_ACCOUNT,
sequence: '123456789',
}),
simulateTransaction: jest.fn().mockResolvedValue({
result: { retval: null },
}),
sendTransaction: jest.fn().mockResolvedValue({
status: 'SUCCESS',
hash: 'mock_hash',
result: { retval: null },
}),
};

executeOptions = {
server: mockServer,
sourceAccount: SOURCE_ACCOUNT,
networkPassphrase: Networks.TESTNET,
fee: '100000',
timeout: 300,
};
});

it('should demonstrate execute integration setup', async () => {
// This test demonstrates the integration setup
expect(contract).toBeDefined();
expect(executeOptions).toBeDefined();
expect(executeOptions.networkPassphrase).toBe(Networks.TESTNET);
});

it('should have executeContract method available', async () => {
expect(typeof contract.executeContract).toBe('function');
});

it('should have simulateExecute method available', async () => {
expect(typeof contract.simulateExecute).toBe('function');
});
});

/**
* Example of how to set up and run real integration tests:
*
* 1. Install Stellar CLI and deploy account contract to testnet:
* ```bash
* cd contracts/account
* stellar contract deploy --wasm target/wasm32-unknown-unknown/release/account.wasm --network testnet
* ```
*
* 2. Initialize the contract:
* ```bash
* stellar contract invoke --id <CONTRACT_ID> --fn initialize --arg <OWNER_ADDRESS> --network testnet
* ```
*
* 3. Update the constants above with actual values and replace mock server with:
* ```typescript
* import { SorobanRpc } from '@stellar/stellar-sdk';
* const server = new SorobanRpc.Server('https://soroban-testnet.stellar.org');
* ```
*
* 4. Remove .skip and run the tests:
* ```bash
* pnpm test --filter @ancore/account-abstraction -- execute.integration.test.ts
* ```
*/
123 changes: 123 additions & 0 deletions packages/account-abstraction/src/__tests__/execute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* Unit tests for execute integration.
* Tests XDR encoding, contract execution, and error mapping.
*/

import { xdr, nativeToScVal } from '@stellar/stellar-sdk';
import { AccountContract } from '../account-contract';
import { encodeContractArgs, parseExecuteResult } from '../execute';
import { mapContractError } from '../errors';

describe('execute integration', () => {
const contractId = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM';
let contract: AccountContract;

beforeEach(() => {
contract = new AccountContract(contractId);
});

describe('encodeContractArgs', () => {
it('should encode basic types', () => {
const args = ['hello', 42, true, null];
const encoded = encodeContractArgs(args);

expect(encoded).toHaveLength(4);
expect(encoded[0]).toEqual(nativeToScVal('hello'));
expect(encoded[1]).toEqual(nativeToScVal(42));
expect(encoded[2]).toEqual(nativeToScVal(true));
expect(encoded[3]).toEqual(xdr.ScVal.scvVoid());
});

it('should encode arrays and objects', () => {
const args = [[1, 2, 3], { key: 'value' }];
const encoded = encodeContractArgs(args);

expect(encoded).toHaveLength(2);
expect(encoded[0]).toEqual(nativeToScVal([1, 2, 3]));
expect(encoded[1]).toEqual(nativeToScVal({ key: 'value' }));
});

it('should handle undefined as void', () => {
const args = [undefined];
const encoded = encodeContractArgs(args);

expect(encoded).toHaveLength(1);
expect(encoded[0]).toEqual(xdr.ScVal.scvVoid());
});

it('should throw on encoding errors', () => {
const circular: Record<string, unknown> = {};
circular.self = circular;

expect(() => encodeContractArgs([circular])).toThrow(/Converting circular structure to JSON/);
});
});

describe('parseExecuteResult', () => {
it('should parse basic types', () => {
const stringVal = nativeToScVal('hello');
const numberVal = nativeToScVal(42);
const boolVal = nativeToScVal(true);

expect(parseExecuteResult(stringVal)).toBe('hello');
expect(parseExecuteResult(numberVal)).toBe(42n); // Numbers become BigInt
expect(parseExecuteResult(boolVal)).toBe(true);
});

it('should parse complex types', () => {
const arrayVal = nativeToScVal([1, 2, 3]);
const objectVal = nativeToScVal({ key: 'value' });

expect(parseExecuteResult(arrayVal)).toEqual([1n, 2n, 3n]); // Numbers become BigInt
expect(parseExecuteResult(objectVal)).toEqual({ key: 'value' });
});

it('should throw on parsing errors', () => {
const invalidScVal = {} as unknown as xdr.ScVal;

expect(() => parseExecuteResult(invalidScVal)).toThrow(/Failed to parse contract result/);
});
});

describe('error mapping integration', () => {
it('should map contract-specific errors correctly', () => {
const testCases = [
{ message: 'Already initialized', expectedError: 'AlreadyInitializedError' },
{ message: 'Not initialized', expectedError: 'NotInitializedError' },
{ message: 'Invalid nonce', expectedError: 'InvalidNonceError' },
{ message: 'unauthorized access', expectedError: 'UnauthorizedError' },
{ message: 'unknown error', expectedError: 'ContractInvocationError' },
];

testCases.forEach(({ message, expectedError }) => {
const mappedError = mapContractError(message, new Error(message));
expect(mappedError.constructor.name).toBe(expectedError);
});
});
});

describe('AccountContract integration methods', () => {
it('should have executeContract method', () => {
expect(typeof contract.executeContract).toBe('function');
});

it('should have simulateExecute method', () => {
expect(typeof contract.simulateExecute).toBe('function');
});

it('should build execute invocation correctly', () => {
const invocation = contract.execute(
'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM',
'transfer',
[
nativeToScVal('GCKFBEIYTKP6RCZX6DSQF22OLNXY2SOGLVUQ6RGE4VW6HKPOLJZX6YTV'),
nativeToScVal(1000),
],
1
);

expect(invocation.method).toBe('execute');
expect(invocation.args).toHaveLength(4);
});
Comment on lines +99 to +121
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

These tests never drive the new execution helpers.

Lines 100-121 only check method presence and invocation shape. They stay green even if executeContract() / simulateExecute() mis-handle mocked RPC responses, return the wrong parsed type, or skip error mapping, so the core behavior added in this PR is still unprotected. Please add mocked-server cases that actually exercise both helpers through success and failure paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/account-abstraction/src/__tests__/execute.test.ts` around lines 99 -
121, The tests only check presence and invocation shape and must be extended to
actually exercise the new helpers: add unit tests that call
contract.executeContract and contract.simulateExecute against a mocked RPC
server (or mocked response handler used by the code) covering both success and
failure flows; for success, mock a typical RPC response and assert the returned
value is the correctly parsed type (matching the library's expected parsed
result), and for failure, mock error responses and assert the helpers map and
throw the mapped error (or return the mapped failure) rather than raw RPC
payloads—use the function names executeContract and simulateExecute and the same
mock setup used elsewhere in tests to locate where to inject the mocked-server
cases.

});
});
57 changes: 39 additions & 18 deletions packages/account-abstraction/src/account-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
*/

import type { SessionKey } from '@ancore/types';
import {
Account,
Contract,
TransactionBuilder,
xdr,
} from '@stellar/stellar-sdk';
import { Account, Contract, TransactionBuilder, xdr } from '@stellar/stellar-sdk';
import { mapContractError } from './errors';
import {
addressToScVal,
Expand All @@ -21,6 +16,12 @@ import {
symbolToScVal,
u64ToScVal,
} from './xdr-utils';
import {
executeContract,
simulateExecute,
type ExecuteOptions,
type ExecuteResult,
} from './execute';

/** Options for read calls (getOwner, getNonce, getSessionKey) when using a server */
export interface AccountContractReadOptions {
Expand Down Expand Up @@ -67,12 +68,7 @@ export class AccountContract {
* Build invocation for execute(to, function, args, expected_nonce).
* Caller must pass the current nonce (e.g. from getNonce()) for replay protection.
*/
execute(
to: string,
fn: string,
args: xdr.ScVal[],
expectedNonce: number
): InvocationArgs {
execute(to: string, fn: string, args: xdr.ScVal[], expectedNonce: number): InvocationArgs {
return {
method: 'execute',
args: [
Expand Down Expand Up @@ -184,6 +180,34 @@ export class AccountContract {
return scValToOptionalSessionKey(result);
}

/**
* Execute a contract method with full transaction submission.
* Encodes arguments, submits transaction, and returns typed result.
*/
async executeContract<T = unknown>(
to: string,
functionName: string,
args: unknown[],
expectedNonce: number,
options: ExecuteOptions
): Promise<ExecuteResult<T>> {
return executeContract(this, to, functionName, args, expectedNonce, options);
}

/**
* Simulate a contract execution without submitting the transaction.
* Useful for testing and gas estimation.
*/
async simulateExecute<T = unknown>(
to: string,
functionName: string,
args: unknown[],
expectedNonce: number,
options: Omit<ExecuteOptions, 'fee'>
): Promise<T> {
return simulateExecute(this, to, functionName, args, expectedNonce, options);
}

/**
* Simulate a read-only contract call and return the result ScVal.
* Throws typed errors on contract/host errors.
Expand All @@ -197,10 +221,7 @@ export class AccountContract {
const { server, sourceAccount } = options;

const accountResponse = await server.getAccount(sourceAccount);
const account = new Account(
accountResponse.id,
accountResponse.sequence ?? '0'
);
const account = new Account(accountResponse.id, accountResponse.sequence ?? '0');

const txBuilder = new TransactionBuilder(account, {
fee: '100',
Expand All @@ -211,7 +232,7 @@ export class AccountContract {

const raw = txBuilder.build();

const sim: any = await server.simulateTransaction(raw);
const sim: unknown = await server.simulateTransaction(raw);

if (sim && typeof sim === 'object' && ('error' in sim || 'message' in sim)) {
const errMsg =
Expand All @@ -221,7 +242,7 @@ export class AccountContract {
throw mapContractError(String(errMsg), sim);
}

const result = (sim as any)?.result?.retval as xdr.ScVal | undefined;
const result = (sim as { result?: { retval?: xdr.ScVal } })?.result?.retval;
if (result === undefined) {
throw mapContractError('No return value from simulation', sim);
}
Expand Down
Loading