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
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ try {
}
```

> **Open note:** `checkWhitelist` passes the raw invocation object returned by `contract.call(...)` directly as the `transaction` field to `simulateTransaction` (cast through `as any`), rather than assembling a full `Transaction` via `TransactionBuilder` the way `AssetModule.mint`/`transfer` do. The source itself flags this with a comment ("Cast required depending on SDK version wrapper"), so the exact request shape expected by `simulateTransaction` across `@stellar/stellar-sdk` versions is not fully confirmed — verify against the installed SDK version rather than assuming it's stable.
> **Note:** `checkWhitelist` builds a full `Transaction` via the shared `buildSimulationTransaction` helper (`src/utils/simulation.ts`) before calling `simulateTransaction`, the same way `AssetModule.mint`/`transfer` build theirs — `simulateTransaction` takes a built `Transaction` directly, not a `{ transaction: ... }` wrapper around a bare, unbuilt operation. Since simulation never signs or submits anything, the source account does not need to be real: the client's configured signer is reused when present, otherwise an ephemeral keypair supplies a structurally valid source.

---

Expand Down
84 changes: 65 additions & 19 deletions docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,31 @@ Before migrating, ensure you have:
### Before (Raw Soroban)

```typescript
import { rpc, Contract, nativeToScVal, Keypair, Networks, xdr, scValToNative } from '@stellar/stellar-sdk';
import { rpc, Contract, nativeToScVal, Keypair, Networks, Account, TransactionBuilder, xdr, scValToNative } from '@stellar/stellar-sdk';

const rpcServer = new rpc.Server('https://soroban-testnet.stellar.org');
const contractId = 'C_YOUR_CONTRACT_ID';
const networkPassphrase = Networks.TESTNET;

// Read-only call: check whitelist
// Read-only call: check whitelist.
// simulateTransaction takes a built Transaction, not a bare operation — and
// since simulation never signs or submits, the source account doesn't need
// to be real; any structurally valid keypair works as a placeholder.
const contract = new Contract(contractId);
const call = contract.call('is_whitelisted', nativeToScVal(userAddress, { type: 'address' }));
const result = await rpcServer.simulateTransaction({ transaction: call as any } as any);
const simSourceAccount = new Account(Keypair.random().publicKey(), '0');
const simTx = new TransactionBuilder(simSourceAccount, { fee: '100', networkPassphrase })
.addOperation(call)
.setTimeout(30)
.build();
const result = await rpcServer.simulateTransaction(simTx);
const isWhitelisted = scValToNative(xdr.ScVal.fromXDR(result.result.retval, 'base64'));

// Write call: mint tokens (you must build everything manually)
// ⚠️ Write call: mint tokens — a privileged, state-changing operation.
// `adminKeypair` here must hold issuer/admin authority on the deployed
// contract. NEVER hardcode a real secret key; load it from a secret manager
// or an environment variable excluded from version control, e.g.:
// const adminKeypair = Keypair.fromSecret(process.env.AEGIS_ISSUER_SECRET!);
const sourceAccount = new Account(adminKeypair.publicKey(), '0');
const mintCall = contract.call(
'mint_asset',
Expand All @@ -69,17 +81,21 @@ const response = await rpcServer.sendTransaction(tx);
import { AegisClient } from '@aegis/sdk';
import { Keypair, Networks } from '@stellar/stellar-sdk';

// ⚠️ Privileged operation: `mint` requires a signer with issuer/admin
// authority on the deployed contract. NEVER hardcode a real secret key —
// load it from a secret manager or an environment variable that is
// excluded from version control.
const aegis = new AegisClient({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: Networks.TESTNET,
contractId: 'C_YOUR_CONTRACT_ID',
keypair: Keypair.fromSecret('S...'),
keypair: Keypair.fromSecret(process.env.AEGIS_ISSUER_SECRET!),
});

// Read-only: check whitelist
// Read-only: check whitelist — no keypair required for this call.
const isWhitelisted = await aegis.compliance.checkWhitelist(userAddress);

// Write: mint tokens
// Write: mint tokens (privileged — see warning above)
const txHash = await aegis.asset.mint(recipientAddress, 1000000000);
```

Expand All @@ -99,15 +115,26 @@ const txHash = await aegis.asset.mint(recipientAddress, 1000000000);
### Before

```typescript
import { rpc, Contract, nativeToScVal, xdr, scValToNative } from '@stellar/stellar-sdk';
import { rpc, Contract, nativeToScVal, xdr, scValToNative, Account, TransactionBuilder, Keypair } from '@stellar/stellar-sdk';

async function checkWhitelist(rpcServer: rpc.Server, contractId: string, address: string): Promise<boolean> {
async function checkWhitelist(
rpcServer: rpc.Server,
contractId: string,
networkPassphrase: string,
address: string
): Promise<boolean> {
const contract = new Contract(contractId);
const call = contract.call('is_whitelisted', nativeToScVal(address, { type: 'address' }));

const result = await rpcServer.simulateTransaction({
transaction: call as any,
} as any);
// simulateTransaction takes a built Transaction, not a bare operation.
// Simulation never signs or submits, so a real account isn't required —
// any structurally valid source account works, e.g. a throwaway keypair.
const sourceAccount = new Account(Keypair.random().publicKey(), '0');
const tx = new TransactionBuilder(sourceAccount, { fee: '100', networkPassphrase })
.addOperation(call)
.setTimeout(30)
.build();
const result = await rpcServer.simulateTransaction(tx);

if (rpc.Api.isSimulationSuccess(result) && result.result) {
const parsed = scValToNative(xdr.ScVal.fromXDR(result.result.retval, 'base64'));
Expand All @@ -134,6 +161,10 @@ const isWhitelisted = await aegis.compliance.checkWhitelist(address);

## Minting Tokens

⚠️ **Privileged operation.** `signer` below must hold issuer/admin authority on
the deployed contract. Never hardcode a real secret key — load it from a
secret manager or an environment variable excluded from version control.

### Before

```typescript
Expand Down Expand Up @@ -193,6 +224,11 @@ const txHash = await aegis.asset.mint(recipientAddress, amount);

## Transferring Tokens

⚠️ **Privileged operation.** `signer` below must be the token holder, or must
otherwise be authorized to move the asset per the deployed contract's rules.
Never hardcode a real secret key — load it from a secret manager or an
environment variable excluded from version control.

### Before

```typescript
Expand Down Expand Up @@ -253,17 +289,27 @@ Reading a full portfolio required multiple manual calls and assembly logic:
async function getPortfolio(
rpcServer: rpc.Server,
contractId: string,
networkPassphrase: string,
investorAddress: string
) {
// simulateTransaction takes a built Transaction, not a bare operation.
// Simulation never signs or submits, so a real account isn't required —
// any structurally valid source account works, e.g. a throwaway keypair.
const buildSimTx = (call: any) => {
const sourceAccount = new Account(Keypair.random().publicKey(), '0');
return new TransactionBuilder(sourceAccount, { fee: '100', networkPassphrase })
.addOperation(call)
.setTimeout(30)
.build();
};

// 1. Check KYC
const contract = new Contract(contractId);
const whitelistCall = contract.call(
'is_whitelisted',
nativeToScVal(investorAddress, { type: 'address' })
);
const whitelistResult = await rpcServer.simulateTransaction({
transaction: whitelistCall as any,
} as any);
const whitelistResult = await rpcServer.simulateTransaction(buildSimTx(whitelistCall));
const isKycApproved =
rpc.Api.isSimulationSuccess(whitelistResult) && whitelistResult.result
? scValToNative(xdr.ScVal.fromXDR(whitelistResult.result.retval, 'base64'))
Expand All @@ -274,9 +320,7 @@ async function getPortfolio(
'balance',
nativeToScVal(investorAddress, { type: 'address' })
);
const balanceResult = await rpcServer.simulateTransaction({
transaction: balanceCall as any,
} as any);
const balanceResult = await rpcServer.simulateTransaction(buildSimTx(balanceCall));
const balance =
rpc.Api.isSimulationSuccess(balanceResult) && balanceResult.result
? scValToNative(xdr.ScVal.fromXDR(balanceResult.result.retval, 'base64'))
Expand Down Expand Up @@ -323,7 +367,9 @@ Raw Soroban error handling requires manual checks at every step:

```typescript
try {
const result = await rpcServer.simulateTransaction({ transaction: call } as any);
// `tx` here is a built Transaction (see the Compliance section above) —
// simulateTransaction does not accept a bare operation or a wrapper object.
const result = await rpcServer.simulateTransaction(tx);
if (rpc.Api.isSimulationSuccess(result) && result.result) {
return scValToNative(xdr.ScVal.fromXDR(result.result.retval, 'base64'));
}
Expand Down
5 changes: 4 additions & 1 deletion docs/reviewer-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ Maintainers and reviewers should use this guide to ensure high standards of qual

## 5. Security, Compliance & Safety

- **Secret Key Protection:** Secret keys or private seeds (`S...`) are NEVER committed to version control or hardcoded in tests/examples.
- **Secret Key Protection:** Secret keys or private seeds (`S...`) are NEVER committed to version control or hardcoded in tests/examples. Any example that constructs a `Keypair` for a signing/write operation must load it from an environment variable or secret manager (e.g. `Keypair.fromSecret(process.env.AEGIS_ISSUER_SECRET!)`), not a literal string, and must carry an explicit "never hardcode a real secret" warning comment.
- **Admin/Privileged Examples Are Labelled:** Any example demonstrating `mint`, `transfer`, or another operation that requires issuer/admin authority on the contract must be visibly marked as privileged (e.g. a `⚠️ Privileged operation` note) so it isn't mistaken for a safe default to copy into a read-only context. See the README's [Quickstart](../README.md#quickstart) (read-only, no keypair) vs. [Privileged Operations](../README.md#privileged-operations-admin--issuer) split for the pattern to follow.
- **RPC Request Shape:** Examples and source that call `simulateTransaction` pass a built `Transaction` (via `Account` + `TransactionBuilder`, see `src/utils/simulation.ts`) directly — never a bare contract-call operation or a `{ transaction: ... }` wrapper object cast through `as any`.
- **RWA Protocol Compliance:** Compliance and whitelist-gated behaviors (e.g., KYC checks, transfer restrictions) maintain security guarantees and accurate disclaimers.
- **Input Validation:** Public endpoints validate user inputs (public keys, contract IDs, transaction parameters) prior to RPC invocation.
- **Examples Audit Trail:** `README.md`, `docs/migration-guide.md`, and `examples/migration/*.ts` are the current canonical examples reviewed against the criteria above (see issue #66). When adding a new example, review it against this list before merging.

---

Expand Down
28 changes: 24 additions & 4 deletions examples/migration/compliance-before-after.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@
*
* Shows the equivalent raw Soroban call vs Aegis SDK usage.
*/
import { rpc, Contract, nativeToScVal, xdr, scValToNative } from '@stellar/stellar-sdk';
import {
rpc,
Contract,
nativeToScVal,
xdr,
scValToNative,
Account,
TransactionBuilder,
Keypair,
} from '@stellar/stellar-sdk';
import { AegisClient } from '@aegis/sdk';

// ============================================================
Expand All @@ -13,6 +22,7 @@ import { AegisClient } from '@aegis/sdk';
async function checkWhitelistRaw(
rpcServer: rpc.Server,
contractId: string,
networkPassphrase: string,
address: string
): Promise<boolean> {
const contract = new Contract(contractId);
Expand All @@ -22,9 +32,19 @@ async function checkWhitelistRaw(
);

try {
const result = await rpcServer.simulateTransaction({
transaction: call as any,
} as any);
// simulateTransaction takes a built Transaction, not a bare operation.
// Simulation never signs or submits, so a real account isn't required —
// any structurally valid source account works, e.g. a throwaway keypair.
const sourceAccount = new Account(Keypair.random().publicKey(), '0');
const tx = new TransactionBuilder(sourceAccount, {
fee: '100',
networkPassphrase,
})
.addOperation(call)
.setTimeout(30)
.build();

const result = await rpcServer.simulateTransaction(tx);

if (rpc.Api.isSimulationSuccess(result) && result.result) {
const parsed = scValToNative(
Expand Down
42 changes: 32 additions & 10 deletions examples/migration/error-handling-before-after.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,40 @@
* Shows how raw Soroban error handling compares to the Aegis SDK's
* structured error approach.
*/
import { rpc, Contract, nativeToScVal, xdr, scValToNative } from '@stellar/stellar-sdk';
import {
rpc,
Contract,
nativeToScVal,
xdr,
scValToNative,
Account,
TransactionBuilder,
Keypair,
} from '@stellar/stellar-sdk';
import { AegisClient, PortfolioError } from '@aegis/sdk';
import type { InvestorPortfolio } from '@aegis/sdk';

// Simulation never signs or submits, so a real account isn't required — any
// structurally valid source account works, e.g. a throwaway keypair.
function buildSimulationTx(networkPassphrase: string, call: any) {
const sourceAccount = new Account(Keypair.random().publicKey(), '0');
return new TransactionBuilder(sourceAccount, {
fee: '100',
networkPassphrase,
})
.addOperation(call)
.setTimeout(30)
.build();
}

// ============================================================
// BEFORE: Raw Soroban — Manual Error Handling
// ============================================================

async function checkWhitelistRaw(
rpcServer: rpc.Server,
contractId: string,
networkPassphrase: string,
address: string
): Promise<boolean> {
const contract = new Contract(contractId);
Expand All @@ -24,9 +47,9 @@ async function checkWhitelistRaw(
);

try {
const result = await rpcServer.simulateTransaction({
transaction: call as any,
} as any);
// simulateTransaction takes a built Transaction, not a bare operation.
const tx = buildSimulationTx(networkPassphrase, call);
const result = await rpcServer.simulateTransaction(tx);

if (rpc.Api.isSimulationSuccess(result) && result.result) {
return scValToNative(
Expand Down Expand Up @@ -69,6 +92,7 @@ async function checkWhitelistSDK(
async function getPortfolioRaw(
rpcServer: rpc.Server,
contractId: string,
networkPassphrase: string,
investorAddress: string
) {
const contract = new Contract(contractId);
Expand All @@ -78,9 +102,8 @@ async function getPortfolioRaw(
'is_whitelisted',
nativeToScVal(investorAddress, { type: 'address' })
);
const whitelistResult = await rpcServer.simulateTransaction({
transaction: whitelistCall as any,
} as any);
const whitelistTx = buildSimulationTx(networkPassphrase, whitelistCall);
const whitelistResult = await rpcServer.simulateTransaction(whitelistTx);

const isKycApproved =
rpc.Api.isSimulationSuccess(whitelistResult) && whitelistResult.result
Expand All @@ -93,9 +116,8 @@ async function getPortfolioRaw(
'balance',
nativeToScVal(investorAddress, { type: 'address' })
);
const balanceResult = await rpcServer.simulateTransaction({
transaction: balanceCall as any,
} as any);
const balanceTx = buildSimulationTx(networkPassphrase, balanceCall);
const balanceResult = await rpcServer.simulateTransaction(balanceTx);

const balance =
rpc.Api.isSimulationSuccess(balanceResult) && balanceResult.result
Expand Down
6 changes: 5 additions & 1 deletion examples/migration/mint-transfer-before-after.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,18 @@ async function transferTokensRaw(

// ============================================================
// AFTER: Aegis SDK — Mint & Transfer
// ⚠️ Privileged operation: mint/transfer require a signing keypair with
// issuer/admin authority on the deployed contract. NEVER hardcode a real
// secret key — load it from an environment variable or secret manager
// that is excluded from version control.
// ============================================================

async function mintAndTransferSDK() {
const aegis = new AegisClient({
rpcUrl: 'https://soroban-testnet.stellar.org',
networkPassphrase: Networks.TESTNET,
contractId: 'C_YOUR_CONTRACT_ID',
keypair: Keypair.fromSecret('S...'),
keypair: Keypair.fromSecret(process.env.AEGIS_ISSUER_SECRET!),
});

// Mint 1000 tokens (in base units) to a recipient
Expand Down
Loading