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
4 changes: 4 additions & 0 deletions apps/web/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ module.exports = {
"^linkora-sdk$": "<rootDir>/../../packages/sdk/src/index.ts",
"^@linkora/types/src/(.*)$": "<rootDir>/../../packages/types/src/$1",
"^@linkora/types$": "<rootDir>/../../packages/types/src/index.ts",
// linkora-sdk's source uses ESM-style ".js" relative imports that
// resolve to sibling ".ts" files at build time; strip the extension so
// Jest's resolver falls through to moduleFileExtensions.
"^(\\.{1,2}/.*)\\.js$": "$1",
},
transform: {
"^.+\\.(ts|tsx)$": [
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/app/profile/[address]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { signTransaction } from "@stellar/freighter-api";
import { LinkoraClient } from "linkora-sdk";
import { buildSignAndSubmit } from "@/lib/tx";
import { addToBlockedList, removeFromBlockedList } from "@/lib/blockedStore";

const CONTRACT_ID = process.env.NEXT_PUBLIC_CONTRACT_ID || "CDUMMY";
const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL || "https://soroban-testnet.stellar.org";
Expand Down
82 changes: 46 additions & 36 deletions apps/web/src/lib/tx.test.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,63 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { signAndSubmitTransaction, buildSignAndSubmit } from "./tx";
import { signTransaction } from "@stellar/freighter-api";
import {
TransactionBuilder,
BASE_FEE,
Contract,
Address,
Account,
rpc as StellarRpc,
} from "@stellar/stellar-sdk";

// Mock Freighter API
vi.mock("@stellar/freighter-api", () => ({
signTransaction: vi.fn(),
jest.mock("@stellar/freighter-api", () => ({
signTransaction: jest.fn(),
}));

// Mock Stellar SDK
vi.mock("@stellar/stellar-sdk", async (importOriginal) => {
const actual = await importOriginal<typeof import("@stellar/stellar-sdk")>();
// Mock Stellar SDK. `Server` always resolves to the same instance so that
// `new StellarRpc.Server(...)` inside tx.ts returns the object the test
// configures via `mockServer` below, instead of an unrelated fresh mock.
jest.mock("@stellar/stellar-sdk", () => {
const actual = jest.requireActual("@stellar/stellar-sdk");
const mockServerInstance = {
getAccount: jest.fn(),
simulateTransaction: jest.fn(),
sendTransaction: jest.fn(),
getTransaction: jest.fn(),
};
// `buildSignAndSubmit` constructs a real TransactionBuilder (`new
// TransactionBuilder(...)`), while `signAndSubmitTransaction` only calls
// the static `fromXDR`. Extend the real class so `new` keeps working, and
// override just the static method the latter needs to mock.
class MockTransactionBuilder extends actual.TransactionBuilder {}
MockTransactionBuilder.fromXDR = jest.fn();

return {
...actual,
TransactionBuilder: {
fromXDR: vi.fn(),
},
TransactionBuilder: MockTransactionBuilder,
rpc: {
Server: vi.fn().mockImplementation(() => ({
getAccount: vi.fn(),
simulateTransaction: vi.fn(),
sendTransaction: vi.fn(),
getTransaction: vi.fn(),
})),
Server: jest.fn(() => mockServerInstance),
Api: {
isSimulationError: vi.fn(),
isSimulationError: jest.fn(),
},
assembleTransaction: vi.fn(),
assembleTransaction: jest.fn(),
},
};
});

describe("Transaction Utility Functions", () => {
const mockConfig = {
contractId: "CDUMMY",
contractId: "CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526",
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: "Test SDF Network ; September 2015",
};

beforeEach(() => {
vi.clearAllMocks();
jest.clearAllMocks();
});

afterEach(() => {
vi.restoreAllMocks();
jest.restoreAllMocks();
});

describe("signAndSubmitTransaction", () => {
Expand Down Expand Up @@ -115,9 +123,10 @@ describe("Transaction Utility Functions", () => {

describe("buildSignAndSubmit", () => {
it("should build, sign, and submit contract method call", async () => {
const mockAccount = {
sequence: "1234567890",
};
const mockAccount = new Account(
"GCMEDW2SHDHZC3YKI3ZQ574VZPDDHR5SZDYPPZLQVR2V56X7UOYX7ZMO",
"1234567890"
);
const mockSimulated = {
minResourceFee: "100",
transactionData: null,
Expand All @@ -135,8 +144,8 @@ describe("Transaction Utility Functions", () => {
(mockServer.simulateTransaction as any).mockResolvedValue(mockSimulated);
(StellarRpc.Api.isSimulationError as any).mockReturnValue(false);
(StellarRpc.assembleTransaction as any).mockReturnValue({
build: vi.fn().mockReturnValue({
toXDR: vi.fn().mockReturnValue("unsigned-xdr"),
build: jest.fn().mockReturnValue({
toXDR: jest.fn().mockReturnValue("unsigned-xdr"),
}),
});
(signTransaction as any).mockResolvedValue(mockSignedXdr);
Expand All @@ -146,13 +155,13 @@ describe("Transaction Utility Functions", () => {
});

const args = [
Address.fromString("GABC123").toScVal(),
Address.fromString("GDEF456").toScVal(),
Address.fromString("GCMEDW2SHDHZC3YKI3ZQ574VZPDDHR5SZDYPPZLQVR2V56X7UOYX7ZMO").toScVal(),
Address.fromString("GBNM7FGFC5BCY6VN6UIFNBYUGNTSLR446YDTWY2BIQU2MHIMAXP2SUM6").toScVal(),
];

const result = await buildSignAndSubmit("test_method", args, "GABC123", mockConfig);
const result = await buildSignAndSubmit("test_method", args, "GCMEDW2SHDHZC3YKI3ZQ574VZPDDHR5SZDYPPZLQVR2V56X7UOYX7ZMO", mockConfig);

expect(mockServer.getAccount).toHaveBeenCalledWith("GABC123");
expect(mockServer.getAccount).toHaveBeenCalledWith("GCMEDW2SHDHZC3YKI3ZQ574VZPDDHR5SZDYPPZLQVR2V56X7UOYX7ZMO");
expect(mockServer.simulateTransaction).toHaveBeenCalled();
expect(signTransaction).toHaveBeenCalled();
expect(mockServer.sendTransaction).toHaveBeenCalled();
Expand All @@ -161,9 +170,10 @@ describe("Transaction Utility Functions", () => {
});

it("should throw error if simulation fails", async () => {
const mockAccount = {
sequence: "1234567890",
};
const mockAccount = new Account(
"GCMEDW2SHDHZC3YKI3ZQ574VZPDDHR5SZDYPPZLQVR2V56X7UOYX7ZMO",
"1234567890"
);
const mockSimulated = {
error: "Simulation error",
};
Expand All @@ -174,11 +184,11 @@ describe("Transaction Utility Functions", () => {
(StellarRpc.Api.isSimulationError as any).mockReturnValue(true);

const args = [
Address.fromString("GABC123").toScVal(),
Address.fromString("GDEF456").toScVal(),
Address.fromString("GCMEDW2SHDHZC3YKI3ZQ574VZPDDHR5SZDYPPZLQVR2V56X7UOYX7ZMO").toScVal(),
Address.fromString("GBNM7FGFC5BCY6VN6UIFNBYUGNTSLR446YDTWY2BIQU2MHIMAXP2SUM6").toScVal(),
];

await expect(buildSignAndSubmit("test_method", args, "GABC123", mockConfig)).rejects.toThrow(
await expect(buildSignAndSubmit("test_method", args, "GCMEDW2SHDHZC3YKI3ZQ574VZPDDHR5SZDYPPZLQVR2V56X7UOYX7ZMO", mockConfig)).rejects.toThrow(
"Transaction simulation failed"
);
});
Expand All @@ -190,7 +200,7 @@ describe("Transaction Utility Functions", () => {
// The old bug was: const _txXdr = client.likePost(...); // XDR discarded

const mockConfig = {
contractId: "CDUMMY",
contractId: "CAAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQCAIBAEAQC526",
rpcUrl: "https://soroban-testnet.stellar.org",
networkPassphrase: "Test SDF Network ; September 2015",
};
Expand Down
57 changes: 41 additions & 16 deletions packages/contracts/contracts/linkora-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ mod validation;
pub use errors::{ContractError, RentError};
use validation::{
validate_address_list, validate_amount, validate_gov_parameter, validate_non_default_address,
validate_protocol_fee, validate_pubkey_32, validate_report_verdict, validate_signature, validate_u32_range,
validate_username, MAX_BIO_LEN, MAX_CONTENT_LEN, MAX_FEE_BPS, MAX_QUORUM,
validate_protocol_fee, validate_pubkey_32, validate_report_verdict,
validate_reporter_can_report, validate_signature, validate_u32_range, validate_username,
MAX_BIO_LEN, MAX_CONTENT_LEN, MAX_FEE_BPS, MAX_QUORUM,
};

// ── Storage Key Enum ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -65,7 +66,7 @@ pub enum StorageKey {
PostReportersIdx(u64, u32), // persistent: (post_id, seq) -> Address (Count is ReportCount)
PostTipCooldownsCount(u64), // persistent: post_id -> u32
PostTipCooldownsIdx(u64, u32), // persistent: (post_id, seq) -> Address
UpgradeProposal, // instance: staged WASM upgrade proposal
UpgradeProposal, // instance: staged WASM upgrade proposal
}

// ── Instance-storage key constants (small scalars, not contracttype) ──────────
Expand Down Expand Up @@ -1104,7 +1105,7 @@ impl LinkoraContract {
Self::bump(&env, &author_key);
}

let remaining_entries: u32 = f_count + following_count + author_posts.len() as u32;
let remaining_entries: u32 = f_count + following_count + author_posts.len();

if f_count == 0 && following_count == 0 && author_posts.is_empty() {
env.storage().persistent().remove(&tombstone_key);
Expand Down Expand Up @@ -3458,13 +3459,20 @@ impl LinkoraContract {
upgrader.require_auth();
validate_non_default_address(&env, "upgrader", &upgrader);
Self::require_role(&env, &upgrader, Role::Upgrader);
require_with_error!(&env, new_wasm_hash != BytesN::from_array(&env, &[0u8; 32]), "wasm hash must not be empty");
require_with_error!(
&env,
new_wasm_hash != BytesN::from_array(&env, &[0u8; 32]),
"wasm hash must not be empty"
);
let proposed_ledger = env.ledger().sequence();
env.storage().instance().set(&StorageKey::UpgradeProposal, &UpgradeProposal {
new_wasm_hash,
proposed_ledger,
executable_ledger: proposed_ledger.saturating_add(UPGRADE_TIMELOCK_LEDGERS),
});
env.storage().instance().set(
&StorageKey::UpgradeProposal,
&UpgradeProposal {
new_wasm_hash,
proposed_ledger,
executable_ledger: proposed_ledger.saturating_add(UPGRADE_TIMELOCK_LEDGERS),
},
);
}

/// Executes the previously proposed contract WASM upgrade after the timelock.
Expand All @@ -3474,15 +3482,32 @@ impl LinkoraContract {
validate_non_default_address(&env, "upgrader", &upgrader);
Self::require_role(&env, &upgrader, Role::Upgrader);
Self::require_not_paused(&env);
let proposal: UpgradeProposal = env.storage().instance().get(&StorageKey::UpgradeProposal).expect("upgrade not proposed");
require_with_error!(&env, env.ledger().sequence() >= proposal.executable_ledger, "upgrade timelock not elapsed");
let proposal: UpgradeProposal = env
.storage()
.instance()
.get(&StorageKey::UpgradeProposal)
.expect("upgrade not proposed");
require_with_error!(
&env,
env.ledger().sequence() >= proposal.executable_ledger,
"upgrade timelock not elapsed"
);
let mut state: ContractState = env.storage().instance().get(&CONTRACT_STATE).unwrap();
state.version = state.version.checked_add(1).expect("contract version overflow");
state.version = state
.version
.checked_add(1)
.expect("contract version overflow");
state.implementation_wasm_hash = Some(proposal.new_wasm_hash.clone());
env.storage().instance().set(&CONTRACT_STATE, &state);
env.deployer().update_current_contract_wasm(proposal.new_wasm_hash.clone());
env.storage().instance().remove(&StorageKey::UpgradeProposal);
ContractUpgraded { new_wasm_hash: proposal.new_wasm_hash }.publish(&env);
env.deployer()
.update_current_contract_wasm(proposal.new_wasm_hash.clone());
env.storage()
.instance()
.remove(&StorageKey::UpgradeProposal);
ContractUpgraded {
new_wasm_hash: proposal.new_wasm_hash,
}
.publish(&env);
}

/// Deprecated immediate-upgrade entrypoint. Upgrades must use
Expand Down
6 changes: 4 additions & 2 deletions packages/contracts/contracts/linkora-contracts/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1365,7 +1365,10 @@ fn test_get_pool_admins_returns_some_for_existing_pool() {
);

let admins = client.get_pool_admins(&pool_id);
assert!(admins.is_some(), "get_pool_admins must return Some for an existing pool");
assert!(
admins.is_some(),
"get_pool_admins must return Some for an existing pool"
);
let admins = admins.unwrap();
assert_eq!(admins.len(), 2);
assert!(admins.iter().any(|a| a == pool_admin1));
Expand Down Expand Up @@ -8078,4 +8081,3 @@ fn test_batch_cleanup_post_emits_event_summary() {
client.batch_cleanup_post(&post_id, &10);
assert!(client.get_post(&post_id).is_none());
}

4 changes: 2 additions & 2 deletions packages/contracts/contracts/token-factory/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,12 @@ impl TokenFactoryContract {
);
require_with_error!(
&env,
name.len() > 0 && name.len() <= MAX_NAME_LEN,
!name.is_empty() && name.len() <= MAX_NAME_LEN,
"name must be 1-64 characters"
);
require_with_error!(
&env,
symbol.len() > 0 && symbol.len() <= MAX_SYMBOL_LEN,
!symbol.is_empty() && symbol.len() <= MAX_SYMBOL_LEN,
"symbol must be 1-16 characters"
);

Expand Down
3 changes: 2 additions & 1 deletion packages/contracts/contracts/token-factory/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ use soroban_sdk::{testutils::Address as _, Address, BytesN, Env, String};
// ── Helpers ───────────────────────────────────────────────────────────────────

/// Placeholder 32-byte hash used when we don't need real WASM semantics.
/// Must be non-zero: `initialize`/`update_token_wasm` reject the all-zero hash.
fn dummy_wasm_hash(env: &Env) -> BytesN<32> {
BytesN::from_array(env, &[0u8; 32])
BytesN::from_array(env, &[0xABu8; 32])
}

fn setup(env: &Env) -> (TokenFactoryContractClient<'_>, Address, BytesN<32>) {
Expand Down
7 changes: 6 additions & 1 deletion packages/sdk/src/__tests__/simulation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,12 @@ describe("LinkoraClient simulation and fee injection", () => {
_isSuccess: true,
minResourceFee: "10000",
transactionData: null,
result: { retval: null },
// One simulated result per operation, each carrying its own auth
// entries — the shape `buildMultiOpTx` requires since #1250.
result: [
{ auth: [], retval: null },
{ auth: [], retval: null },
],
};
mockSimulateTransaction.mockResolvedValue(mockResult);

Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/__tests__/write.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jest.mock("@stellar/stellar-base", () => ({
})),
scValToNative: jest.fn(),
TransactionBuilder: jest.fn(() => ({ addOperation: mockAddOperation })),
Account: jest.fn(),
Account: jest.fn((accountId: string, sequence: string) => ({ _accountId: accountId, sequence })),
Keypair: { random: jest.fn(() => ({ publicKey: () => "GWRITEKEYXXXXXXXXXXXXXXXXXXXXXXXXXX" })) },
xdr: {},
}));
Expand Down
12 changes: 11 additions & 1 deletion packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import { GovParameter } from "./generated/types.js";
import type { GovProposal } from "./generated/types.js";
import { ConnectionHealthMonitor, HealthCheckConfig, ConnectionStatusCallback } from "./health.js";
import { fetchWithTimeout } from "./utils/fetch.js";
import type { QueueSigner, RunOptions } from "./queue.js";
import {
createRpcClientAdapter,
type QueueSigner,
type RpcClient,
type RunOptions,
} from "./queue.js";
import { submitTransaction } from "./submit.js";

const { isSimulationError, isSimulationSuccess } = rpc.Api;
Expand Down Expand Up @@ -262,6 +267,11 @@ export class LinkoraClient extends GeneratedLinkoraClient {
return new rpc.Server(this._rpcUrl, { allowHttp: this._allowHttp });
}

/** Build a string-XDR {@link RpcClient} adapter for use with `TransactionQueue`. */
createRpcClient(): RpcClient {
return createRpcClientAdapter(this.createRpcServer(), this._networkPassphrase);
}

/**
* Convenience method to sign and submit a transaction using the TransactionQueue.
*
Expand Down
Loading
Loading