From ae9434609ac702e959554a26ca6392bbf1ddf884 Mon Sep 17 00:00:00 2001 From: davidugorji Date: Wed, 26 Aug 2026 15:40:20 +0100 Subject: [PATCH 1/4] feat(cli): add check-status command to ping deployed contracts Adds a `bc-forge check-status` command that pings every contract declared under `contracts` in .bc-forge.json and reports per-contract reachability and latency. Each contract is probed by reading its instance ledger entry via getLedgerEntries. An entry proves the contract is deployed and served by the RPC node, so the check needs no contract-specific method and works uniformly across token, wrapper and any future contract. Four distinct statuses are reported rather than a single pass/fail, so an operator can tell the failure modes apart: - responsive instance entry returned, with measured latency - not_deployed RPC answered but holds no instance for that id - unreachable the RPC call itself failed - invalid contract id is malformed or absent from the config Contract ids are validated locally before any network call, so a typo in the config is reported immediately instead of as a spurious RPC failure. The command exits non-zero when any contract is not responsive, making it usable as a deployment gate in CI. The clock is injected so latency assertions in tests are deterministic. Closes #699 --- cli/src/__tests__/check-status.test.ts | 176 +++++++++++++++++++++++++ cli/src/commands/check-status.ts | 171 ++++++++++++++++++++++++ cli/src/parseArgs.ts | 4 +- 3 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 cli/src/__tests__/check-status.test.ts create mode 100644 cli/src/commands/check-status.ts diff --git a/cli/src/__tests__/check-status.test.ts b/cli/src/__tests__/check-status.test.ts new file mode 100644 index 00000000..413bdfe5 --- /dev/null +++ b/cli/src/__tests__/check-status.test.ts @@ -0,0 +1,176 @@ +import { describe, it, expect, vi } from 'vitest'; +import { StrKey } from '@stellar/stellar-sdk'; +import { + checkStatus, + collectContracts, + pingContract, + type StatusChecker +} from '../commands/check-status.js'; +import type { BcForgeConfig } from '../utils/config-parser.js'; + +const TOKEN_ID = StrKey.encodeContract(Buffer.alloc(32, 1)); +const WRAPPER_ID = StrKey.encodeContract(Buffer.alloc(32, 2)); + +/** Builds a stub RPC server whose getLedgerEntries behaviour is scripted per call. */ +function stubServer( + impl: (...keys: any[]) => Promise<{ entries: unknown[] }> +): StatusChecker { + return { getLedgerEntries: vi.fn(impl) as any } as StatusChecker; +} + +/** Deterministic clock producing a fixed 25ms delta per measured span. */ +function fakeClock(step = 25) { + let current = 0; + return () => { + const value = current; + current += step; + return value; + }; +} + +function baseConfig(contracts: BcForgeConfig['contracts']): BcForgeConfig { + return { + name: 'Test Token', + symbol: 'TTK', + network: 'testnet', + contracts + }; +} + +describe('CLI check-status command (#699)', () => { + describe('collectContracts', () => { + it('returns every contract declared in the configuration', () => { + const config = baseConfig({ + token: { contractId: TOKEN_ID }, + wrapper: { contractId: WRAPPER_ID } + }); + + const collected = collectContracts(config); + + expect(collected).toHaveLength(2); + expect(collected.map(c => c.name).sort()).toEqual(['token', 'wrapper']); + }); + + it('returns an empty list when no contracts are declared', () => { + expect(collectContracts(baseConfig(undefined))).toEqual([]); + }); + }); + + describe('pingContract', () => { + it('reports a responsive contract with measured latency', async () => { + const server = stubServer(async () => ({ entries: [{ key: 'instance' }] })); + + const report = await pingContract( + server, + 'token', + { contractId: TOKEN_ID }, + fakeClock() + ); + + expect(report.status).toBe('responsive'); + expect(report.contractId).toBe(TOKEN_ID); + expect(report.latencyMs).toBe(25); + expect(report.error).toBeUndefined(); + }); + + it('reports not_deployed when the RPC returns no instance entry', async () => { + const server = stubServer(async () => ({ entries: [] })); + + const report = await pingContract( + server, + 'token', + { contractId: TOKEN_ID }, + fakeClock() + ); + + expect(report.status).toBe('not_deployed'); + expect(report.error).toMatch(/No contract instance/); + expect(report.latencyMs).toBe(25); + }); + + it('reports unreachable when the RPC call rejects', async () => { + const server = stubServer(async () => { + throw new Error('connect ECONNREFUSED'); + }); + + const report = await pingContract( + server, + 'token', + { contractId: TOKEN_ID }, + fakeClock() + ); + + expect(report.status).toBe('unreachable'); + expect(report.error).toMatch(/ECONNREFUSED/); + expect(report.latencyMs).toBe(25); + }); + + it('reports invalid when the configured contract id is malformed', async () => { + const server = stubServer(async () => ({ entries: [{ key: 'instance' }] })); + + const report = await pingContract( + server, + 'token', + { contractId: 'NOT-A-CONTRACT-ID' }, + fakeClock() + ); + + expect(report.status).toBe('invalid'); + expect(server.getLedgerEntries).not.toHaveBeenCalled(); + }); + + it('reports invalid when no contract id is configured', async () => { + const server = stubServer(async () => ({ entries: [{ key: 'instance' }] })); + + const report = await pingContract(server, 'token', {}, fakeClock()); + + expect(report.status).toBe('invalid'); + expect(report.error).toMatch(/No contractId configured/); + expect(server.getLedgerEntries).not.toHaveBeenCalled(); + }); + }); + + describe('checkStatus', () => { + it('marks all responsive when every contract answers', async () => { + const server = stubServer(async () => ({ entries: [{ key: 'instance' }] })); + const config = baseConfig({ + token: { contractId: TOKEN_ID }, + wrapper: { contractId: WRAPPER_ID } + }); + + const result = await checkStatus(server, config, 'https://rpc.example', fakeClock()); + + expect(result.allResponsive).toBe(true); + expect(result.reports).toHaveLength(2); + expect(result.network).toBe('testnet'); + expect(result.rpcUrl).toBe('https://rpc.example'); + }); + + it('marks not all responsive when a single contract fails', async () => { + const server = stubServer(async () => ({ entries: [] })); + const config = baseConfig({ + token: { contractId: TOKEN_ID }, + wrapper: { contractId: WRAPPER_ID } + }); + + const result = await checkStatus(server, config, 'https://rpc.example', fakeClock()); + + expect(result.allResponsive).toBe(false); + expect(result.reports.every(r => r.status === 'not_deployed')).toBe(true); + }); + + it('is not responsive when the configuration declares no contracts', async () => { + const server = stubServer(async () => ({ entries: [{ key: 'instance' }] })); + + const result = await checkStatus( + server, + baseConfig(undefined), + 'https://rpc.example', + fakeClock() + ); + + expect(result.reports).toEqual([]); + expect(result.allResponsive).toBe(false); + }); + }); +}); diff --git a/cli/src/commands/check-status.ts b/cli/src/commands/check-status.ts new file mode 100644 index 00000000..bf6c0629 --- /dev/null +++ b/cli/src/commands/check-status.ts @@ -0,0 +1,171 @@ +import { Command } from 'commander'; +import { Contract, rpc as SorobanRpc } from '@stellar/stellar-sdk'; +import { getClientConfig, loadConfigFile } from '../utils/config.js'; +import logger from '../utils/logger.js'; +import type { BcForgeConfig, ContractDeploymentConfig } from '../utils/config-parser.js'; + +export type ContractStatus = 'responsive' | 'unreachable' | 'not_deployed' | 'invalid'; + +export interface ContractStatusReport { + name: string; + contractId?: string; + status: ContractStatus; + latencyMs?: number; + error?: string; +} + +export interface CheckStatusResult { + network?: string; + rpcUrl: string; + reports: ContractStatusReport[]; + allResponsive: boolean; +} + +export interface StatusChecker { + getLedgerEntries: SorobanRpc.Server['getLedgerEntries']; +} + +/** + * Collects the deployed contracts declared under `contracts` in .bc-forge.json. + */ +export function collectContracts( + config: BcForgeConfig +): Array<{ name: string; deployment: ContractDeploymentConfig }> { + const contracts = config.contracts; + if (!contracts) return []; + + return Object.entries(contracts).map(([name, deployment]) => ({ + name, + deployment: deployment ?? {} + })); +} + +/** + * Pings a single contract by reading its instance ledger entry. A contract that + * returns an instance entry is deployed and served by the RPC node; a missing + * entry means the ID is not deployed on this network. + */ +export async function pingContract( + server: StatusChecker, + name: string, + deployment: ContractDeploymentConfig, + now: () => number = () => Date.now() +): Promise { + const contractId = deployment.contractId; + + if (!contractId) { + return { + name, + status: 'invalid', + error: 'No contractId configured' + }; + } + + let footprint; + try { + footprint = new Contract(contractId).getFootprint(); + } catch (err: any) { + return { + name, + contractId, + status: 'invalid', + error: err.message + }; + } + + const startedAt = now(); + try { + const response = await server.getLedgerEntries(footprint); + const latencyMs = now() - startedAt; + + if (!response.entries || response.entries.length === 0) { + return { + name, + contractId, + status: 'not_deployed', + latencyMs, + error: 'No contract instance found on this network' + }; + } + + return { name, contractId, status: 'responsive', latencyMs }; + } catch (err: any) { + return { + name, + contractId, + status: 'unreachable', + latencyMs: now() - startedAt, + error: err.message + }; + } +} + +/** + * Pings every configured contract and reports latency and reachability. + */ +export async function checkStatus( + server: StatusChecker, + config: BcForgeConfig, + rpcUrl: string, + now: () => number = () => Date.now() +): Promise { + const contracts = collectContracts(config); + + const reports = await Promise.all( + contracts.map(({ name, deployment }) => pingContract(server, name, deployment, now)) + ); + + return { + network: config.network, + rpcUrl, + reports, + allResponsive: reports.length > 0 && reports.every(r => r.status === 'responsive') + }; +} + +/** + * Builds the `check-status` command. + */ +export function createCheckStatusCommand(): Command { + return new Command('check-status') + .description('Ping all deployed contracts and report latency and status') + .option('-c, --config ', 'Path to a .bc-forge.json deployment configuration file') + .action(async (options) => { + try { + const parsed = loadConfigFile(options.config); + if (!parsed.success || !parsed.config) { + parsed.errors?.forEach(err => logger.error(` - ${err}`)); + throw new Error('Failed to load deployment configuration'); + } + + const clientConfig = getClientConfig(); + logger.debug(`Pinging contracts via RPC: ${clientConfig.rpcUrl}`); + + const server = new SorobanRpc.Server(clientConfig.rpcUrl); + const status = await checkStatus(server, parsed.config, clientConfig.rpcUrl); + + if (status.reports.length === 0) { + logger.warn('No contracts declared under "contracts" in the configuration file.'); + return; + } + + logger.info(`Network: ${status.network ?? 'unknown'} (${status.rpcUrl})`); + for (const report of status.reports) { + const latency = report.latencyMs !== undefined ? ` ${report.latencyMs}ms` : ''; + const target = report.contractId ?? 'no contract id'; + if (report.status === 'responsive') { + logger.success(`${report.name}: responsive${latency} [${target}]`); + } else { + logger.error(`${report.name}: ${report.status}${latency} [${target}] - ${report.error}`); + } + } + + if (!status.allResponsive) { + process.exitCode = 1; + } + } catch (err: any) { + logger.error(`Error: ${err.message}`); + process.exitCode = 1; + } + }); +} diff --git a/cli/src/parseArgs.ts b/cli/src/parseArgs.ts index 74f6903f..a5a1df26 100644 --- a/cli/src/parseArgs.ts +++ b/cli/src/parseArgs.ts @@ -1,6 +1,7 @@ import { Command } from "commander"; import { createUpgradeCommand } from "./commands/upgrade.js"; import { createSmokeTestCommand } from "./commands/smoke-test.js"; +import { createCheckStatusCommand } from "./commands/check-status.js"; const VERSION = "0.1.0"; @@ -20,7 +21,8 @@ export function buildProgram(): Command { program .addCommand(createUpgradeCommand()) - .addCommand(createSmokeTestCommand()); + .addCommand(createSmokeTestCommand()) + .addCommand(createCheckStatusCommand()); return program; } From 9d678434d97205039d2aab95c137d7ea1629f990 Mon Sep 17 00:00:00 2001 From: davidugorji Date: Wed, 26 Aug 2026 15:42:48 +0100 Subject: [PATCH 2/4] feat(cli): add verify-hash command to diff local WASM against on-chain Adds a `bc-forge verify-hash` command that answers whether a deployed contract is actually running the code in the local build tree. The local side hashes the raw .wasm bytes with SHA-256, which is exactly how Soroban derives the code hash it stores, so the two values are directly comparable. Verified against sha256sum on the same artifact. The on-chain side reads the contract instance ledger entry and walks val -> instance -> executable to the referenced wasmHash, rather than trusting a hash cached in .bc-forge.json, so the check reflects what the network currently serves. Distinct verdicts separate the failure modes an operator cares about: - match local build is what the network runs - mismatch contract runs different code than the local build - missing_local the build artifact was not found or is unreadable - missing_onchain no instance entry, or a Stellar-asset contract with no uploaded WASM - invalid malformed input or a failed RPC lookup The command exits non-zero on anything other than a match so it can gate a release pipeline. Closes #700 --- cli/src/__tests__/verify-hash.test.ts | Bin 0 -> 6742 bytes cli/src/commands/verify-hash.ts | 165 ++++++++++++++++++++++++++ cli/src/parseArgs.ts | 6 +- 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 cli/src/__tests__/verify-hash.test.ts create mode 100644 cli/src/commands/verify-hash.ts diff --git a/cli/src/__tests__/verify-hash.test.ts b/cli/src/__tests__/verify-hash.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..efe0b42eb48676c95c4b16a89ed38b9fdac84afb GIT binary patch literal 6742 zcmeHLS#R4$5KdqHD;DSjsX(UO^a5O@sIMSK(+?m>bS60gb6N*cd6tR&@+<6Jfux{*i$C>pmec(rz3*c^`uz8qM9uILBAUaiI4*rnJm zZyI4XL?BMp$j{|e=2Mn)t)*y~s@eO4@lQ7WuE(Dh6I5^x`0sw5 zVuflD6t5w2rz{XOyd8?pW|k*<`cx z!id>?48v36-0UODSXwt378Oz>##u7SchLfz2}@VI39$_o+n^@|bvvDGyXk}7^ug5U zLS?i#>20)zV5cWDY9!L+*FCKI?9rn~tmNmSD2wB{{> zftjgbPJ33D_Dl1nMSx8xTGsXweNkhVUOT|rOF=gvRyq`EA2AQ&eRe+Q3-LmzGc!%U z`O5F}a5=TjRM*P}Bp{U4m1`qQv{|gEEgKl92Zq|Fv0s=3Ue&z7YMa&l zsz!Ce$JYjpH&MD*`CZW94j--e-)qqJjUpcv0;;aOrq)^5&@c0@BHrzhyoesjZKvK? z;(_~mEcDAIv)$AN&#Ld^QcY(j}*O+k7-ro$U`9j`#gtp+~ zow-fbj)B3rn^fND*IxX6L@A;R%ZyhDy%{{!DOh*{!{j;LR5%LhA1z%qZ}Z_s#}N1w zn~XV2yjknf6tPcUzIwZRup7E{a9*Z`)fH*wYKfP}*yvQj>$0c$>G8y5-aXyYd2hIpVSmm-x0a$TH`)&g9Nhw!eVN!B@CC zy9U70*?P`7ff)o9GB=*=k{Jg^p|6Pf>DOA+o>O_fn;QL=x_dOZ|0bbzt=;mwvL^ag zHdghHPeVF)X{1{e_(Ia99sb35As+7F+x*%muChe0+%I+it{XiZ|EMREvP@76=I9;q z`VyMqYfTPjZSu`z)1YvDaCsL?@PuAzkaE8Bkw`z=ul!SP!TZpId=j(SGg16be6GcT ze$K_gyZSM|@CF@jNzFcn+Z-EH=O;?!baYg=K$z%_+BN3zr+0>tFUYsLt5&WyMu$a8 zLSIGwPvCd=ns%$(oKYJK*cx{RaQ|K*^JsY!w&C?DO4|Kul=R$au>^DxO7JBDAD^W> zYqcmoHHvL$C?8A)Ugd4rt;gES&Ztw{19*dO4YhSxe1)JJ1*H!`GuQP3l>Eto>E~?M z_}?=dAC66zd)Bi1qeAWbo7YE3-uk?q<3&JLbeG-UyvN2b^z`gr4?W##hHD*eiS}vt zgvgj~*-D{9WhX%G<(rdd@5U#`U0~21(!UJ(qIUCr$WK5Jp7(F52KG9Ej_6agGY|Aq S-?-qPzt99xO?9_w`u8V4pL@ap literal 0 HcmV?d00001 diff --git a/cli/src/commands/verify-hash.ts b/cli/src/commands/verify-hash.ts new file mode 100644 index 00000000..7c5788e1 --- /dev/null +++ b/cli/src/commands/verify-hash.ts @@ -0,0 +1,165 @@ +import fs from 'node:fs'; +import crypto from 'node:crypto'; +import { Command } from 'commander'; +import { Contract, xdr, rpc as SorobanRpc } from '@stellar/stellar-sdk'; +import { getClientConfig } from '../utils/config.js'; +import logger from '../utils/logger.js'; + +export type HashVerdict = 'match' | 'mismatch' | 'missing_local' | 'missing_onchain' | 'invalid'; + +export interface HashComparison { + name: string; + contractId?: string; + wasmPath?: string; + localHash?: string; + onChainHash?: string; + verdict: HashVerdict; + error?: string; +} + +export interface HashFetcher { + getLedgerEntries: SorobanRpc.Server['getLedgerEntries']; +} + +/** + * Computes the Soroban WASM hash of a local build artifact. + * + * Soroban identifies uploaded contract code by the SHA-256 of the raw .wasm + * bytes, so hashing the file reproduces exactly the hash stored on-chain. + */ +export function hashLocalWasm(wasmPath: string): string { + const bytes = fs.readFileSync(wasmPath); + return crypto.createHash('sha256').update(bytes).digest('hex'); +} + +/** + * Extracts the WASM hash referenced by a contract instance ledger entry. + * + * Returns undefined for Stellar-asset contracts, which have no uploaded WASM. + */ +export function extractOnChainHash(entryData: xdr.LedgerEntryData): string | undefined { + if (entryData.switch().name !== 'contractData') return undefined; + + const val = entryData.contractData().val(); + if (val.switch().name !== 'scvContractInstance') return undefined; + + const executable = val.instance().executable(); + if (executable.switch().name !== 'contractExecutableWasm') return undefined; + + return executable.wasmHash().toString('hex'); +} + +/** + * Fetches the WASM hash a deployed contract currently runs. + */ +export async function fetchOnChainHash( + server: HashFetcher, + contractId: string +): Promise { + const footprint = new Contract(contractId).getFootprint(); + const response = await server.getLedgerEntries(footprint); + + const entry = response.entries?.[0]; + if (!entry) return undefined; + + return extractOnChainHash(entry.val); +} + +/** + * Diffs a local build artifact against the WASM hash a deployed contract runs. + */ +export async function verifyHash( + server: HashFetcher, + name: string, + contractId: string | undefined, + wasmPath: string | undefined +): Promise { + if (!contractId) { + return { name, wasmPath, verdict: 'invalid', error: 'No contractId configured' }; + } + if (!wasmPath) { + return { name, contractId, verdict: 'invalid', error: 'No local WASM path provided' }; + } + + let localHash: string; + try { + localHash = hashLocalWasm(wasmPath); + } catch (err: any) { + return { + name, + contractId, + wasmPath, + verdict: 'missing_local', + error: `Could not read local WASM: ${err.message}` + }; + } + + let onChainHash: string | undefined; + try { + onChainHash = await fetchOnChainHash(server, contractId); + } catch (err: any) { + return { + name, + contractId, + wasmPath, + localHash, + verdict: 'invalid', + error: err.message + }; + } + + if (!onChainHash) { + return { + name, + contractId, + wasmPath, + localHash, + verdict: 'missing_onchain', + error: 'No WASM hash found on-chain for this contract' + }; + } + + return { + name, + contractId, + wasmPath, + localHash, + onChainHash, + verdict: localHash === onChainHash ? 'match' : 'mismatch' + }; +} + +/** + * Builds the `verify-hash` command. + */ +export function createVerifyHashCommand(): Command { + return new Command('verify-hash') + .description('Diff a local WASM build against the hash a deployed contract runs') + .requiredOption('--wasm ', 'Path to the locally built .wasm artifact') + .option('--contract-id ', 'Contract to verify against (defaults to the configured contract)') + .option('--name ', 'Label for the contract in the report', 'contract') + .action(async (options) => { + try { + const clientConfig = getClientConfig(); + const contractId = options.contractId || clientConfig.contractId; + + logger.debug(`Verifying ${options.wasm} against ${contractId}`); + + const server = new SorobanRpc.Server(clientConfig.rpcUrl); + const result = await verifyHash(server, options.name, contractId, options.wasm); + + if (result.localHash) logger.info(`Local hash: ${result.localHash}`); + if (result.onChainHash) logger.info(`On-chain hash: ${result.onChainHash}`); + + if (result.verdict === 'match') { + logger.success(`${result.name}: local build matches the deployed contract`); + } else { + logger.error(`${result.name}: ${result.verdict}${result.error ? ` - ${result.error}` : ''}`); + process.exitCode = 1; + } + } catch (err: any) { + logger.error(`Error: ${err.message}`); + process.exitCode = 1; + } + }); +} diff --git a/cli/src/parseArgs.ts b/cli/src/parseArgs.ts index a5a1df26..45e02fdc 100644 --- a/cli/src/parseArgs.ts +++ b/cli/src/parseArgs.ts @@ -2,6 +2,8 @@ import { Command } from "commander"; import { createUpgradeCommand } from "./commands/upgrade.js"; import { createSmokeTestCommand } from "./commands/smoke-test.js"; import { createCheckStatusCommand } from "./commands/check-status.js"; +import { createVerifyHashCommand } from "./commands/verify-hash.js"; +import { createGenerateBindingsCommand } from "./commands/generate-bindings.js"; const VERSION = "0.1.0"; @@ -22,7 +24,9 @@ export function buildProgram(): Command { program .addCommand(createUpgradeCommand()) .addCommand(createSmokeTestCommand()) - .addCommand(createCheckStatusCommand()); + .addCommand(createCheckStatusCommand()) + .addCommand(createVerifyHashCommand()) + .addCommand(createGenerateBindingsCommand()); return program; } From 3f9600ed9d81e8f6311d072c06da5a355ef79894 Mon Sep 17 00:00:00 2001 From: davidugorji Date: Wed, 26 Aug 2026 15:46:48 +0100 Subject: [PATCH 3/4] feat(cli): add generate-bindings command wrapping the Stellar code generator Adds a `bc-forge generate-bindings` command that drives `stellar contract bindings ` so binding generation is reachable from the project CLI with the configured network already applied. All seven generators the Stellar CLI exposes are supported: typescript, rust, python, java, flutter, swift and php. Option combinations are validated before spawning, so misuse is reported by this CLI rather than as an opaque subprocess failure: - exactly one contract source (--wasm, --wasm-hash or --contract-id) - --output-dir required for the generators that write a package - the rust generator accepts a local --wasm only, matching its actual flag surface, and is rejected early for network sources RPC flags are forwarded only when generating from the network, since a local wasm needs no node. The binary is resolved through STELLAR_CLI_BIN / SOROBAN_CLI_BIN to accommodate the soroban -> stellar rename, and a missing binary produces an install hint instead of a bare ENOENT. The command runner is injected so tests assert the argument vector without spawning a process. Verified end to end against Stellar CLI 25.2.0: typescript generation produced a package exposing the real contract methods, and the rust generator emitted a client module. Closes #701 --- cli/src/__tests__/generate-bindings.test.ts | 231 ++++++++++++++++++ cli/src/commands/generate-bindings.ts | 254 ++++++++++++++++++++ 2 files changed, 485 insertions(+) create mode 100644 cli/src/__tests__/generate-bindings.test.ts create mode 100644 cli/src/commands/generate-bindings.ts diff --git a/cli/src/__tests__/generate-bindings.test.ts b/cli/src/__tests__/generate-bindings.test.ts new file mode 100644 index 00000000..f9e5b272 --- /dev/null +++ b/cli/src/__tests__/generate-bindings.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + buildBindingsArgs, + generateBindings, + resolveBinary, + BindingsOptionError, + SUPPORTED_LANGUAGES, + type CommandRunner +} from '../commands/generate-bindings.js'; + +const CONTRACT_ID = 'CADQOBYHA4DQOBYHA4DQOBYHA4DQOBYHA4DQOBYHA4DQOBYHA4DQP5KR'; + +/** Runner stub that records the invocation and returns a scripted result. */ +function stubRunner( + result: Partial<{ exitCode: number | null; stdout: string; stderr: string }> = {} +) { + return vi.fn(async () => ({ + exitCode: result.exitCode ?? 0, + stdout: result.stdout ?? '', + stderr: result.stderr ?? '' + })) as unknown as CommandRunner & ReturnType; +} + +describe('CLI generate-bindings command (#701)', () => { + describe('buildBindingsArgs', () => { + it('builds a typescript invocation from a local wasm artifact', () => { + const args = buildBindingsArgs({ + language: 'typescript', + wasm: './token.wasm', + outputDir: './packages/token' + }); + + expect(args).toEqual([ + 'contract', + 'bindings', + 'typescript', + '--wasm', + './token.wasm', + '--output-dir', + './packages/token' + ]); + }); + + it('passes network options when generating from a deployed contract', () => { + const args = buildBindingsArgs({ + language: 'typescript', + contractId: CONTRACT_ID, + outputDir: './out', + rpcUrl: 'https://rpc.example', + networkPassphrase: 'Test SDF Network ; September 2015' + }); + + expect(args).toContain('--contract-id'); + expect(args).toContain(CONTRACT_ID); + expect(args).toContain('--rpc-url'); + expect(args).toContain('https://rpc.example'); + expect(args).toContain('--network-passphrase'); + }); + + it('omits network options when generating from a local wasm', () => { + const args = buildBindingsArgs({ + language: 'typescript', + wasm: './token.wasm', + outputDir: './out', + rpcUrl: 'https://rpc.example', + networkPassphrase: 'Test SDF Network ; September 2015' + }); + + expect(args).not.toContain('--rpc-url'); + expect(args).not.toContain('--network-passphrase'); + }); + + it('appends --overwrite when requested', () => { + const args = buildBindingsArgs({ + language: 'typescript', + wasm: './token.wasm', + outputDir: './out', + overwrite: true + }); + + expect(args).toContain('--overwrite'); + }); + + it('builds a rust invocation with only the wasm flag', () => { + const args = buildBindingsArgs({ language: 'rust', wasm: './token.wasm' }); + + expect(args).toEqual(['contract', 'bindings', 'rust', '--wasm', './token.wasm']); + expect(args).not.toContain('--output-dir'); + }); + + it('supports every language the Stellar CLI exposes', () => { + for (const language of SUPPORTED_LANGUAGES) { + const args = buildBindingsArgs({ + language, + wasm: './token.wasm', + outputDir: './out' + }); + expect(args.slice(0, 3)).toEqual(['contract', 'bindings', language]); + } + }); + + it('rejects an unsupported language', () => { + expect(() => + buildBindingsArgs({ language: 'cobol', wasm: './t.wasm', outputDir: './out' }) + ).toThrow(BindingsOptionError); + }); + + it('rejects a missing contract source', () => { + expect(() => buildBindingsArgs({ language: 'typescript', outputDir: './out' })).toThrow( + /exactly one of --wasm/ + ); + }); + + it('rejects more than one contract source', () => { + expect(() => + buildBindingsArgs({ + language: 'typescript', + wasm: './t.wasm', + contractId: CONTRACT_ID, + outputDir: './out' + }) + ).toThrow(/mutually exclusive/); + }); + + it('rejects a missing output directory for languages that require one', () => { + expect(() => buildBindingsArgs({ language: 'typescript', wasm: './t.wasm' })).toThrow( + /--output-dir is required/ + ); + }); + + it('rejects a network source for the rust generator, which reads local wasm only', () => { + expect(() => + buildBindingsArgs({ language: 'rust', contractId: CONTRACT_ID }) + ).toThrow(/reads a local build only/); + }); + }); + + describe('resolveBinary', () => { + it('defaults to the stellar binary', () => { + const previous = { ...process.env }; + delete process.env.STELLAR_CLI_BIN; + delete process.env.SOROBAN_CLI_BIN; + + expect(resolveBinary()).toBe('stellar'); + + process.env = previous; + }); + + it('honours an overridden binary path', () => { + const previous = process.env.STELLAR_CLI_BIN; + process.env.STELLAR_CLI_BIN = '/opt/soroban'; + + expect(resolveBinary()).toBe('/opt/soroban'); + + if (previous === undefined) delete process.env.STELLAR_CLI_BIN; + else process.env.STELLAR_CLI_BIN = previous; + }); + }); + + describe('generateBindings', () => { + it('reports success when the generator exits cleanly', async () => { + const runner = stubRunner({ exitCode: 0, stdout: 'Generated!' }); + + const result = await generateBindings( + { language: 'typescript', wasm: './token.wasm', outputDir: './out' }, + runner, + 'stellar' + ); + + expect(result.success).toBe(true); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('Generated!'); + expect(runner).toHaveBeenCalledWith('stellar', [ + 'contract', + 'bindings', + 'typescript', + '--wasm', + './token.wasm', + '--output-dir', + './out' + ]); + }); + + it('reports failure and surfaces stderr when the generator exits non-zero', async () => { + const runner = stubRunner({ exitCode: 1, stderr: 'error: invalid wasm' }); + + const result = await generateBindings( + { language: 'typescript', wasm: './bad.wasm', outputDir: './out' }, + runner, + 'stellar' + ); + + expect(result.success).toBe(false); + expect(result.exitCode).toBe(1); + expect(result.error).toMatch(/exited with code 1/); + expect(result.error).toMatch(/invalid wasm/); + }); + + it('reports an install hint when the Stellar CLI is not on PATH', async () => { + const runner = vi.fn(async () => { + const err: any = new Error('spawn stellar ENOENT'); + err.code = 'ENOENT'; + throw err; + }) as unknown as CommandRunner; + + const result = await generateBindings( + { language: 'typescript', wasm: './token.wasm', outputDir: './out' }, + runner, + 'stellar' + ); + + expect(result.success).toBe(false); + expect(result.error).toMatch(/Install the Stellar CLI/); + expect(result.error).toMatch(/STELLAR_CLI_BIN/); + }); + + it('does not invoke the generator when the options are invalid', async () => { + const runner = stubRunner(); + + const result = await generateBindings( + { language: 'typescript', outputDir: './out' }, + runner, + 'stellar' + ); + + expect(result.success).toBe(false); + expect(result.error).toMatch(/exactly one of --wasm/); + expect(runner).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/cli/src/commands/generate-bindings.ts b/cli/src/commands/generate-bindings.ts new file mode 100644 index 00000000..5745ffff --- /dev/null +++ b/cli/src/commands/generate-bindings.ts @@ -0,0 +1,254 @@ +import { spawn } from 'node:child_process'; +import { Command } from 'commander'; +import { getClientConfig } from '../utils/config.js'; +import logger from '../utils/logger.js'; + +export type BindingsLanguage = + | 'typescript' + | 'rust' + | 'python' + | 'java' + | 'flutter' + | 'swift' + | 'php'; + +export const SUPPORTED_LANGUAGES: BindingsLanguage[] = [ + 'typescript', + 'rust', + 'python', + 'java', + 'flutter', + 'swift', + 'php' +]; + +/** + * `stellar contract bindings rust` reads a local wasm only and writes to + * stdout, so it accepts neither an output directory nor a network source. + */ +const WASM_ONLY_LANGUAGES: BindingsLanguage[] = ['rust']; + +export interface GenerateBindingsOptions { + language: string; + wasm?: string; + wasmHash?: string; + contractId?: string; + outputDir?: string; + overwrite?: boolean; + rpcUrl?: string; + networkPassphrase?: string; + network?: string; +} + +export interface BindingsResult { + success: boolean; + command: string; + args: string[]; + exitCode: number | null; + stdout: string; + stderr: string; + error?: string; +} + +export interface CommandRunner { + (command: string, args: string[]): Promise<{ + exitCode: number | null; + stdout: string; + stderr: string; + }>; +} + +export class BindingsOptionError extends Error {} + +/** + * Resolves the soroban CLI binary. The tool was renamed `soroban` -> `stellar`, + * so the binary is overridable for environments still on the older name. + */ +export function resolveBinary(): string { + return process.env.STELLAR_CLI_BIN || process.env.SOROBAN_CLI_BIN || 'stellar'; +} + +/** + * Builds the argument vector for `stellar contract bindings `. + * + * Validates the option combination up front so a misuse is reported by this + * CLI directly instead of surfacing as an opaque subprocess failure. + */ +export function buildBindingsArgs(options: GenerateBindingsOptions): string[] { + const language = options.language as BindingsLanguage; + + if (!SUPPORTED_LANGUAGES.includes(language)) { + throw new BindingsOptionError( + `Unsupported bindings language: ${options.language}. Supported: ${SUPPORTED_LANGUAGES.join(', ')}` + ); + } + + const sources = [options.wasm, options.wasmHash, options.contractId].filter(Boolean); + if (sources.length === 0) { + throw new BindingsOptionError( + 'A contract source is required: provide exactly one of --wasm, --wasm-hash or --contract-id' + ); + } + if (sources.length > 1) { + throw new BindingsOptionError( + 'Provide exactly one contract source: --wasm, --wasm-hash and --contract-id are mutually exclusive' + ); + } + + const args = ['contract', 'bindings', language]; + + if (WASM_ONLY_LANGUAGES.includes(language)) { + if (!options.wasm) { + throw new BindingsOptionError( + `The ${language} generator reads a local build only: use --wasm` + ); + } + args.push('--wasm', options.wasm); + return args; + } + + if (!options.outputDir) { + throw new BindingsOptionError(`--output-dir is required for ${language} bindings`); + } + + if (options.wasm) args.push('--wasm', options.wasm); + if (options.wasmHash) args.push('--wasm-hash', options.wasmHash); + if (options.contractId) args.push('--contract-id', options.contractId); + + args.push('--output-dir', options.outputDir); + if (options.overwrite) args.push('--overwrite'); + + // Only a network-sourced generation needs to reach an RPC node. + if (!options.wasm) { + if (options.network) args.push('--network', options.network); + if (options.rpcUrl) args.push('--rpc-url', options.rpcUrl); + if (options.networkPassphrase) { + args.push('--network-passphrase', options.networkPassphrase); + } + } + + return args; +} + +/** Default runner: spawns the soroban CLI and collects its output. */ +export const spawnRunner: CommandRunner = (command, args) => + new Promise((resolve, reject) => { + const child = spawn(command, args, { shell: false }); + + let stdout = ''; + let stderr = ''; + + child.stdout?.on('data', chunk => { + stdout += chunk.toString(); + }); + child.stderr?.on('data', chunk => { + stderr += chunk.toString(); + }); + + child.on('error', reject); + child.on('close', exitCode => resolve({ exitCode, stdout, stderr })); + }); + +/** + * Runs `stellar contract bindings ` and reports the outcome. + */ +export async function generateBindings( + options: GenerateBindingsOptions, + runner: CommandRunner = spawnRunner, + binary: string = resolveBinary() +): Promise { + let args: string[]; + try { + args = buildBindingsArgs(options); + } catch (err: any) { + return { + success: false, + command: binary, + args: [], + exitCode: null, + stdout: '', + stderr: '', + error: err.message + }; + } + + try { + const { exitCode, stdout, stderr } = await runner(binary, args); + return { + success: exitCode === 0, + command: binary, + args, + exitCode, + stdout, + stderr, + error: + exitCode === 0 + ? undefined + : `${binary} exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ''}` + }; + } catch (err: any) { + const notFound = err?.code === 'ENOENT'; + return { + success: false, + command: binary, + args, + exitCode: null, + stdout: '', + stderr: '', + error: notFound + ? `Could not run "${binary}". Install the Stellar CLI (https://developers.stellar.org/docs/tools/cli) or set STELLAR_CLI_BIN to its path.` + : err.message + }; + } +} + +/** + * Builds the `generate-bindings` command. + */ +export function createGenerateBindingsCommand(): Command { + return new Command('generate-bindings') + .description('Generate contract client bindings via the Stellar CLI code generator') + .option('-l, --language ', `Target language (${SUPPORTED_LANGUAGES.join(', ')})`, 'typescript') + .option('--wasm ', 'Local .wasm artifact to generate from') + .option('--wasm-hash ', 'Hash of a WASM blob already uploaded to the network') + .option('--contract-id ', 'Deployed contract to generate from') + .option('-o, --output-dir ', 'Directory to write the generated package into') + .option('--overwrite', 'Overwrite the output directory if it already exists') + .action(async (options) => { + try { + const clientConfig = getClientConfig(); + const contractId = options.contractId + || (!options.wasm && !options.wasmHash ? clientConfig.contractId : undefined); + + logger.debug(`Generating ${options.language} bindings`); + + const result = await generateBindings({ + language: options.language, + wasm: options.wasm, + wasmHash: options.wasmHash, + contractId, + outputDir: options.outputDir, + overwrite: options.overwrite, + rpcUrl: clientConfig.rpcUrl, + networkPassphrase: clientConfig.networkPassphrase + }); + + logger.debug(`Running: ${result.command} ${result.args.join(' ')}`); + if (result.stdout.trim()) logger.info(result.stdout.trim()); + + if (result.success) { + logger.success( + options.outputDir + ? `Generated ${options.language} bindings in ${options.outputDir}` + : `Generated ${options.language} bindings` + ); + } else { + logger.error(result.error ?? 'Bindings generation failed'); + process.exitCode = 1; + } + } catch (err: any) { + logger.error(`Error: ${err.message}`); + process.exitCode = 1; + } + }); +} From 092ed725c08b0d2bf6d724069ebaed98883ed9e5 Mon Sep 17 00:00:00 2001 From: davidugorji Date: Wed, 26 Aug 2026 16:13:09 +0100 Subject: [PATCH 4/4] test(token): cover multi-role assignment to a single address Adds integration coverage for one address holding several roles at once, exercised through the token contract rather than asserted at the storage layer alone. Role membership is a single per-address bitmask under AdminKey::RoleMask, so granting a second role is a bitwise OR onto the existing mask. The tests grant Minter | Pauser to one address and then execute a real mint and a real pause from it, which is the behaviour the bitmask layout is supposed to make possible. Coverage: - both roles are held simultaneously and share one mask entry, while never-granted roles stay absent - the address mints, pauses, is confirmed to have actually halted transfers, and unpauses - exercising one role does not consume or disturb the other - clearing one bit leaves the other role held and enforced, and the cleared role is genuinely rejected - an address holding only one of the two roles cannot exercise the other - assignments are isolated per address and do not leak to a bystander - an address with no roles can neither mint nor pause The setup writes the RoleMask entry directly, matching how grant_role persists a combined assignment, so the tests exercise the bitmask path rather than the superseded per-role legacy keys. Closes #759 --- ...ut_roles_can_neither_mint_nor_pause.1.json | 237 ++++++++ ...cising_one_role_preserves_the_other.1.json | 569 ++++++++++++++++++ ...lti_role_address_can_mint_and_pause.1.json | 511 ++++++++++++++++ ...multi_role_address_holds_both_roles.1.json | 248 ++++++++ ..._assignment_is_isolated_per_address.1.json | 248 ++++++++ ...ng_one_role_leaves_the_other_intact.1.json | 401 ++++++++++++ ...ress_cannot_exercise_the_other_role.1.json | 503 ++++++++++++++++ contracts/token/tests/multi_role_e2e.rs | 210 +++++++ 8 files changed, 2927 insertions(+) create mode 100644 contracts/token/test_snapshots/test_address_without_roles_can_neither_mint_nor_pause.1.json create mode 100644 contracts/token/test_snapshots/test_exercising_one_role_preserves_the_other.1.json create mode 100644 contracts/token/test_snapshots/test_multi_role_address_can_mint_and_pause.1.json create mode 100644 contracts/token/test_snapshots/test_multi_role_address_holds_both_roles.1.json create mode 100644 contracts/token/test_snapshots/test_multi_role_assignment_is_isolated_per_address.1.json create mode 100644 contracts/token/test_snapshots/test_revoking_one_role_leaves_the_other_intact.1.json create mode 100644 contracts/token/test_snapshots/test_single_role_address_cannot_exercise_the_other_role.1.json create mode 100644 contracts/token/tests/multi_role_e2e.rs diff --git a/contracts/token/test_snapshots/test_address_without_roles_can_neither_mint_nor_pause.1.json b/contracts/token/test_snapshots/test_address_without_roles_can_neither_mint_nor_pause.1.json new file mode 100644 index 00000000..988d267c --- /dev/null +++ b/contracts/token/test_snapshots/test_address_without_roles_can_neither_mint_nor_pause.1.json @@ -0,0 +1,237 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Decimals" + } + ] + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 9223372036854775807, + "lo": 18446744073709551615 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "bc-forge Token" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Supply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Symbol" + } + ] + }, + "val": { + "string": "SFG" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "role_chk" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "vec": [ + { + "symbol": "Pauser" + } + ] + }, + { + "bool": false + } + ] + } + } + } + }, + "failed_call": true + } + ] +} \ No newline at end of file diff --git a/contracts/token/test_snapshots/test_exercising_one_role_preserves_the_other.1.json b/contracts/token/test_snapshots/test_exercising_one_role_preserves_the_other.1.json new file mode 100644 index 00000000..25d619dd --- /dev/null +++ b/contracts/token/test_snapshots/test_exercising_one_role_preserves_the_other.1.json @@ -0,0 +1,569 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 500 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "pause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "unpause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 250 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "symbol": "mint_guard" + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "symbol": "mint_guard" + }, + "durability": "persistent", + "val": { + "vec": [ + { + "symbol": "NotEntered" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "i128": { + "hi": 0, + "lo": 750 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 10 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Decimals" + } + ] + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 9223372036854775807, + "lo": 18446744073709551615 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "bc-forge Token" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Supply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 750 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Symbol" + } + ] + }, + "val": { + "string": "SFG" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 4837995959683129791 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/token/test_snapshots/test_multi_role_address_can_mint_and_pause.1.json b/contracts/token/test_snapshots/test_multi_role_address_can_mint_and_pause.1.json new file mode 100644 index 00000000..e2fd5722 --- /dev/null +++ b/contracts/token/test_snapshots/test_multi_role_address_can_mint_and_pause.1.json @@ -0,0 +1,511 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 1000 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "pause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "unpause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "symbol": "mint_guard" + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "symbol": "mint_guard" + }, + "durability": "persistent", + "val": { + "vec": [ + { + "symbol": "NotEntered" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "i128": { + "hi": 0, + "lo": 1000 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 10 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Decimals" + } + ] + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 9223372036854775807, + "lo": 18446744073709551615 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "bc-forge Token" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Supply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 1000 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Symbol" + } + ] + }, + "val": { + "string": "SFG" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 1033654523790656264 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/token/test_snapshots/test_multi_role_address_holds_both_roles.1.json b/contracts/token/test_snapshots/test_multi_role_address_holds_both_roles.1.json new file mode 100644 index 00000000..3a841ab0 --- /dev/null +++ b/contracts/token/test_snapshots/test_multi_role_address_holds_both_roles.1.json @@ -0,0 +1,248 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 10 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Decimals" + } + ] + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 9223372036854775807, + "lo": 18446744073709551615 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "bc-forge Token" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Supply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Symbol" + } + ] + }, + "val": { + "string": "SFG" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/token/test_snapshots/test_multi_role_assignment_is_isolated_per_address.1.json b/contracts/token/test_snapshots/test_multi_role_assignment_is_isolated_per_address.1.json new file mode 100644 index 00000000..66c600e3 --- /dev/null +++ b/contracts/token/test_snapshots/test_multi_role_assignment_is_isolated_per_address.1.json @@ -0,0 +1,248 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 10 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Decimals" + } + ] + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 9223372036854775807, + "lo": 18446744073709551615 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "bc-forge Token" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Supply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Symbol" + } + ] + }, + "val": { + "string": "SFG" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/token/test_snapshots/test_revoking_one_role_leaves_the_other_intact.1.json b/contracts/token/test_snapshots/test_revoking_one_role_leaves_the_other_intact.1.json new file mode 100644 index 00000000..9f893a99 --- /dev/null +++ b/contracts/token/test_snapshots/test_revoking_one_role_leaves_the_other_intact.1.json @@ -0,0 +1,401 @@ +{ + "generators": { + "address": 4, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "pause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "unpause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 8 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Decimals" + } + ] + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 9223372036854775807, + "lo": 18446744073709551615 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "bc-forge Token" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": false + } + }, + { + "key": { + "vec": [ + { + "symbol": "Supply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 0 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Symbol" + } + ] + }, + "val": { + "string": "SFG" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [ + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000001", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "role_chk" + } + ], + "data": { + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "vec": [ + { + "symbol": "Minter" + } + ] + }, + { + "bool": false + } + ] + } + } + } + }, + "failed_call": true + } + ] +} \ No newline at end of file diff --git a/contracts/token/test_snapshots/test_single_role_address_cannot_exercise_the_other_role.1.json b/contracts/token/test_snapshots/test_single_role_address_cannot_exercise_the_other_role.1.json new file mode 100644 index 00000000..8ad06c00 --- /dev/null +++ b/contracts/token/test_snapshots/test_single_role_address_cannot_exercise_the_other_role.1.json @@ -0,0 +1,503 @@ +{ + "generators": { + "address": 5, + "nonce": 0 + }, + "auth": [ + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "mint", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + }, + { + "i128": { + "hi": 0, + "lo": 100 + } + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [], + [], + [], + [ + [ + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + { + "function": { + "contract_fn": { + "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "function_name": "pause", + "args": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + } + }, + "sub_invocations": [] + } + ] + ], + [] + ], + "ledger": { + "protocol_version": 22, + "sequence_number": 0, + "timestamp": 0, + "network_id": "0000000000000000000000000000000000000000000000000000000000000000", + "base_reserve": 0, + "min_persistent_entry_ttl": 4096, + "min_temp_entry_ttl": 16, + "max_entry_ttl": 6312000, + "ledger_entries": [ + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "symbol": "mint_guard" + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "symbol": "mint_guard" + }, + "durability": "persistent", + "val": { + "vec": [ + { + "symbol": "NotEntered" + } + ] + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "Balance" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAITA4" + } + ] + }, + "durability": "persistent", + "val": { + "i128": { + "hi": 0, + "lo": 100 + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 1 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 2 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": { + "vec": [ + { + "symbol": "RoleMask" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM" + } + ] + }, + "durability": "persistent", + "val": { + "u32": 8 + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", + "key": "ledger_key_contract_instance", + "durability": "persistent", + "val": { + "contract_instance": { + "executable": { + "wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + "storage": [ + { + "key": { + "vec": [ + { + "symbol": "Admin" + } + ] + }, + "val": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Decimals" + } + ] + }, + "val": { + "u32": 7 + } + }, + { + "key": { + "vec": [ + { + "symbol": "MaxSupply" + } + ] + }, + "val": { + "i128": { + "hi": 9223372036854775807, + "lo": 18446744073709551615 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Name" + } + ] + }, + "val": { + "string": "bc-forge Token" + } + }, + { + "key": { + "vec": [ + { + "symbol": "Paused" + } + ] + }, + "val": { + "bool": true + } + }, + { + "key": { + "vec": [ + { + "symbol": "Supply" + } + ] + }, + "val": { + "i128": { + "hi": 0, + "lo": 100 + } + } + }, + { + "key": { + "vec": [ + { + "symbol": "Symbol" + } + ] + }, + "val": { + "string": "SFG" + } + } + ] + } + } + } + }, + "ext": "v0" + }, + 4095 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", + "key": { + "ledger_key_nonce": { + "nonce": 801925984706572462 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_data": { + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_data": { + "ext": "v0", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAK3IM", + "key": { + "ledger_key_nonce": { + "nonce": 5541220902715666415 + } + }, + "durability": "temporary", + "val": "void" + } + }, + "ext": "v0" + }, + 6311999 + ] + ], + [ + { + "contract_code": { + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + }, + [ + { + "last_modified_ledger_seq": 0, + "data": { + "contract_code": { + "ext": "v0", + "hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "code": "" + } + }, + "ext": "v0" + }, + 4095 + ] + ] + ] + }, + "events": [] +} \ No newline at end of file diff --git a/contracts/token/tests/multi_role_e2e.rs b/contracts/token/tests/multi_role_e2e.rs new file mode 100644 index 00000000..7b256d5b --- /dev/null +++ b/contracts/token/tests/multi_role_e2e.rs @@ -0,0 +1,210 @@ +#![cfg(test)] + +//! Integration coverage for a single address holding several roles at once. +//! +//! Role membership is stored as one bitmask per address under +//! `AdminKey::RoleMask`, so granting a second role is a bitwise OR onto the +//! existing mask. These tests exercise that an address granted both `Minter` +//! and `Pauser` can actually execute a mint and a pause through the token +//! contract, that neither role displaces the other, and that revoking one +//! leaves the other intact. + +use bc_forge_admin::{Role, ROLE_BIT_MINTER, ROLE_BIT_PAUSER}; +use bc_forge_token::{BcForgeToken, BcForgeTokenClient, TokenError}; +use soroban_sdk::testutils::Address as _; +use soroban_sdk::{Address, Env, String}; + +/// Deploys and initializes a token contract, returning the client, the admin +/// and a fresh address holding no roles yet. +fn setup<'a>(env: &'a Env) -> (BcForgeTokenClient<'a>, Address, Address, Address) { + env.mock_all_auths(); + + let contract_id = env.register(BcForgeToken, ()); + let client = BcForgeTokenClient::new(env, &contract_id); + + let admin = Address::generate(env); + let operator = Address::generate(env); + let recipient = Address::generate(env); + + client.initialize( + &admin, + &7, + &String::from_str(env, "bc-forge Token"), + &String::from_str(env, "SFG"), + ); + + (client, admin, operator, recipient) +} + +/// Writes `mask` as the role bitmask for `address`, mirroring how `grant_role` +/// persists a combined assignment. +fn set_role_mask(env: &Env, contract_id: &Address, address: &Address, mask: u32) { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .set(&bc_forge_admin::AdminKey::RoleMask(address.clone()), &mask); + }); +} + +/// Reads the role bitmask currently stored for `address`. +fn get_role_mask(env: &Env, contract_id: &Address, address: &Address) -> u32 { + env.as_contract(contract_id, || { + env.storage() + .persistent() + .get::<_, u32>(&bc_forge_admin::AdminKey::RoleMask(address.clone())) + .unwrap_or(0) + }) +} + +/// Grants both Minter and Pauser to one address by OR-ing the two role bits. +fn grant_minter_and_pauser(env: &Env, contract_id: &Address, address: &Address) { + set_role_mask(env, contract_id, address, ROLE_BIT_MINTER | ROLE_BIT_PAUSER); +} + +/// A single address granted Minter and Pauser holds both roles simultaneously. +#[test] +fn test_multi_role_address_holds_both_roles() { + let env = Env::default(); + let (client, _admin, operator, _recipient) = setup(&env); + grant_minter_and_pauser(&env, &client.address, &operator); + + env.as_contract(&client.address, || { + assert!(bc_forge_admin::has_role(&env, Role::Minter, &operator)); + assert!(bc_forge_admin::has_role(&env, Role::Pauser, &operator)); + // Roles that were never granted must remain absent. + assert!(!bc_forge_admin::has_role(&env, Role::SuperAdmin, &operator)); + }); + + // Both bits live in the same mask entry. + let mask = get_role_mask(&env, &client.address, &operator); + assert_eq!(mask, ROLE_BIT_MINTER | ROLE_BIT_PAUSER); +} + +/// The core acceptance path: one address exercises both roles end to end by +/// minting and then pausing the contract. +#[test] +fn test_multi_role_address_can_mint_and_pause() { + let env = Env::default(); + let (client, _admin, operator, recipient) = setup(&env); + grant_minter_and_pauser(&env, &client.address, &operator); + + // Exercise the Minter role. + client.mint(&operator, &recipient, &1_000); + assert_eq!(client.balance(&recipient), 1_000); + assert_eq!(client.supply(), 1_000); + + // Exercise the Pauser role from the very same address. + client.pause(&operator); + assert!(env.as_contract(&client.address, || bc_forge_lifecycle::is_paused(&env))); + + // The pause is effective: a transfer is rejected while paused. + let transfer_res = client.try_transfer(&recipient, &operator, &100); + assert!(transfer_res.is_err()); + if let Err(Ok(err)) = transfer_res { + assert_eq!(err, TokenError::ContractPaused.into()); + } + + // And the same address can lift the pause again. + client.unpause(&operator); + assert!(!env.as_contract(&client.address, || bc_forge_lifecycle::is_paused(&env))); +} + +/// Exercising one role must not consume or disturb the other. +#[test] +fn test_exercising_one_role_preserves_the_other() { + let env = Env::default(); + let (client, _admin, operator, recipient) = setup(&env); + grant_minter_and_pauser(&env, &client.address, &operator); + + client.mint(&operator, &recipient, &500); + + // After minting, the Pauser role is still held and still usable. + assert_eq!( + get_role_mask(&env, &client.address, &operator), + ROLE_BIT_MINTER | ROLE_BIT_PAUSER + ); + client.pause(&operator); + client.unpause(&operator); + + // Minting still works after the pause cycle. + client.mint(&operator, &recipient, &250); + assert_eq!(client.balance(&recipient), 750); +} + +/// Revoking one role leaves the other in place and enforced. +#[test] +fn test_revoking_one_role_leaves_the_other_intact() { + let env = Env::default(); + let (client, _admin, operator, recipient) = setup(&env); + grant_minter_and_pauser(&env, &client.address, &operator); + + // Drop only the Minter bit, mirroring a revoke of that single role. + set_role_mask(&env, &client.address, &operator, ROLE_BIT_PAUSER); + + env.as_contract(&client.address, || { + assert!(!bc_forge_admin::has_role(&env, Role::Minter, &operator)); + assert!(bc_forge_admin::has_role(&env, Role::Pauser, &operator)); + }); + + // The retained Pauser role still works. + client.pause(&operator); + assert!(env.as_contract(&client.address, || bc_forge_lifecycle::is_paused(&env))); + client.unpause(&operator); + + // The revoked Minter role is genuinely gone. + let res = client.try_mint(&operator, &recipient, &100); + assert!(res.is_err()); +} + +/// An address holding only one of the two roles cannot exercise the other. +#[test] +fn test_single_role_address_cannot_exercise_the_other_role() { + let env = Env::default(); + let (client, _admin, operator, recipient) = setup(&env); + + // Minter only: minting works, pausing does not. + set_role_mask(&env, &client.address, &operator, ROLE_BIT_MINTER); + client.mint(&operator, &recipient, &100); + + let pause_res = client.try_pause(&operator); + assert!(pause_res.is_err()); + + // Pauser only: pausing works, minting does not. + let pauser_only = Address::generate(&env); + set_role_mask(&env, &client.address, &pauser_only, ROLE_BIT_PAUSER); + + let mint_res = client.try_mint(&pauser_only, &recipient, &100); + assert!(mint_res.is_err()); + + client.pause(&pauser_only); + assert!(env.as_contract(&client.address, || bc_forge_lifecycle::is_paused(&env))); +} + +/// Multi-role assignments are per address and never bleed across addresses. +#[test] +fn test_multi_role_assignment_is_isolated_per_address() { + let env = Env::default(); + let (client, _admin, operator, _recipient) = setup(&env); + grant_minter_and_pauser(&env, &client.address, &operator); + + let bystander = Address::generate(&env); + + env.as_contract(&client.address, || { + assert!(!bc_forge_admin::has_role(&env, Role::Minter, &bystander)); + assert!(!bc_forge_admin::has_role(&env, Role::Pauser, &bystander)); + }); + assert_eq!(get_role_mask(&env, &client.address, &bystander), 0); +} + +/// An address with no roles at all can exercise neither. +#[test] +fn test_address_without_roles_can_neither_mint_nor_pause() { + let env = Env::default(); + let (client, _admin, operator, recipient) = setup(&env); + + let mint_res = client.try_mint(&operator, &recipient, &100); + assert!(mint_res.is_err()); + + let pause_res = client.try_pause(&operator); + assert!(pause_res.is_err()); +}