Asami is a private club to thank and encourage Rootstock bitcoiners.
This repository contains a deliberately small system:
- an upgradeable Foundry contract that stores members and accepted assets and sends one payment per call;
- one Rust binary that indexes events, creates database disbursements, pays members, serves REST data and the landing page, and publishes queued notifications;
- one bilingual React landing page;
- one end-to-end scenario.
There is no token, campaign system, login, wallet connection, GraphQL API, administration UI, or user account system.
- contract/src/Asami.sol — upgradeable membership registry and single-payment treasury.
- src — the asami Rust binary.
- migrations — retained historical migrations plus the additive minimal schema.
- pwa — the public landing page.
- integration_tests — the readable Rust E2E fixture and single club scenario.
- scripts/e2e — a thin launcher for the Rust E2E test.
- scripts/run-dev — a thin launcher for the Rust seeded local environment.
- scripts/deploy — production build and optional Foundry deployment.
Initialize dependencies after cloning:
git submodule update --init --recursive
Foundry's standard forge install workflow keeps Solidity dependencies as pinned Git
submodules under lib/. In this repository those submodules are forge-std,
openzeppelin-contracts, and openzeppelin-contracts-upgradeable. The project's
contract, Rust, frontend, and end-to-end tests are ordinary files in this repository;
none of the test suites are submodules.
Requirements: Rust, Foundry, Node/npm, Docker, and curl.
Run the fast suites:
forge test
cargo test
npm --prefix pwa ci
npm --prefix pwa run build
Run the complete scenario:
scripts/e2e
Explore the seeded landing page interactively:
scripts/run-dev
The E2E is written in Rust and owns its disposable PostgreSQL container, Anvil process, API server, ChromeDriver, and browser lifecycle. The pinned ChromeDriver and Chromium support files live under integration_tests/chromedrivers. On the first run, the matching Chromium executable is downloaded into that fixture directory. It covers member indexing, native and ERC-20 distribution, minimum reconsideration, terminal underfunding, low-gas stopping/refill, unsupported-token alerts, REST responses, and the rendered landing page.
Production binaries are built inside Ubuntu 22.04 so they remain compatible with glibc 2.35 servers. The builder compiles the frontend first because its assets are embedded in the Rust executable:
scripts/release-builder
The compatible single binary is written to release/asami.
Commands:
- asami migrate
- asami api
- asami worker
- asami worker --once
- asami logs
- asami config
- asami x-auth-url --account asami_english
- asami x-auth-complete --account asami_english --redirected-url URL
- asami recover-unsent --transaction-hash 0x...
- asami demo
The API process serves the frontend and these public read-only endpoints:
- GET /members
- GET /assets
- GET /config
- GET /logs
- GET /health
- GET /ready
Copy .env.example into the deployment secret/configuration system. Secrets must never be committed or logged.
Important settings include:
- DATABASE_URL
- ROOTSTOCK_RPC_URL
- ROOTSTOCK_CHAIN_ID
- ASAMI_CONTRACT_ADDRESS
- CONTRACT_DEPLOY_BLOCK
- AUTOMATOR_MNEMONIC
- AUTOMATOR_DERIVATION_PATH (defaults to the legacy
m/44'/60'/0'/0/0path) - DISBURSEMENT_AUTOMATOR_ADDRESS
- MIN_AUTOMATOR_GAS_RESERVE
- GAS_BALANCE_SAFETY_BASIS_POINTS
- FINALITY_DEPTH
- BLOCK_CHUNK_SIZE
- TRANSFER_RECONSIDERATION_LIMIT
- LOW_GAS_TWEET_COOLDOWN_HOURS
- X_DAILY_POST_CAP
- X_CLIENT_ID
- X_CLIENT_SECRET
- X_REDIRECT_URI
- ASAMI_ENCRYPTION_KEY
ASAMI_ENCRYPTION_KEY is a base64-encoded 32-byte key used to encrypt X access and refresh tokens in PostgreSQL.
When AUTOMATOR_MNEMONIC is configured, asami worker reads one BIP-39
passphrase line from standard input when input is piped. This supports production
wrappers such as systemd-ask-password --no-tty | asami worker. When launched
from an interactive terminal, it prompts without echoing instead. The derived
address must equal DISBURSEMENT_AUTOMATOR_ADDRESS; otherwise the worker exits
before processing payouts. AUTOMATOR_PRIVATE_KEY remains available only for
local development and tests.
The passphrase is never accepted through an environment variable.
Set the three role addresses independently:
- PROXY_ADMIN_ADDRESS
- CONTRACT_OWNER_ADDRESS
- DISBURSEMENT_AUTOMATOR_ADDRESS
Production deployment uses the Trezor account at m/44'/137'/0'/0/1. Pass that complete path to Forge; custom derivation paths cannot be combined with --mnemonic-indexes. Run:
DEPLOY_CONTRACT=1 \
DEPLOYER_ADDRESS=0xd9FCAe4315920387f00725C78285D6D41C30b967 \
PROXY_ADMIN_ADDRESS=0xd9FCAe4315920387f00725C78285D6D41C30b967 \
CONTRACT_OWNER_ADDRESS=0xd9FCAe4315920387f00725C78285D6D41C30b967 \
DISBURSEMENT_AUTOMATOR_ADDRESS=0x3e79325b61d941e7996f0a1aad4f66a703e24faa \
ROOTSTOCK_RPC_URL=https://public-node.rsk.co \
scripts/deploy
For routine application deployments after the contracts are already deployed, run:
scripts/build-and-deploy-to root@167.99.18.108
This opens one persistent SSH control session, builds the frontend and compatible
single binary through scripts/release-builder, uploads it with a checksum check,
runs migrations, atomically replaces /home/asami/asami, and restarts both systemd
services. It then asks for the automator wallet passphrase through systemd remote
TTY prompt. SSH authentication is requested only once. Override the remote directory
or unit names with ASAMI_REMOTE_DIR, ASAMI_API_SERVICE, and
ASAMI_WORKER_SERVICE when necessary.
Set DEPLOY_TARGET as well to copy the single release binary. The deployment JSON records the proxy, implementation, ProxyAdmin, owner, automator, chain ID, and deployment block. Foundry records every deployment transaction hash in:
broadcast/Deploy.s.sol/<chain-id>/run-latest.json
Run migrations before starting the new service version. Production normally uses two service commands from the same binary:
asami api
asami worker
Complete the English X OAuth setup once using x-auth-url and x-auth-complete with the asami_english account key.
Each eligible incoming transfer creates one database disbursement and one immutable payout intent per member from the latest synchronized member projection. Every nonzero payout is sent in its own transaction.
The contract has no batch concept, payout identifier, completed-payout mapping, or off-chain disbursement reference. PostgreSQL attempt and receipt records are the only logical payout ledger.
A payout is complete only after a successful receipt is stored. Rootstock payouts are explicitly signed as legacy transactions. Ambiguous submissions retain the exact signed transaction and are reconciled or safely rebroadcast before any later payout can allocate a nonce.
Attempts made by an older binary may lack those signed bytes. After stopping the worker and independently confirming that each transaction hash is absent from the node and explorer, recover them explicitly (repeat the option for multiple hashes):
asami recover-unsent \
--transaction-hash 0xFIRST \
--transaction-hash 0xSECOND
The command checks the receipt, transaction, and pending account nonce before it marks the corresponding payout retryable. It refuses ambiguous recovery.
If contract funds are unavailable when a transfer is reconsidered, the disbursement and calculated intents are retained as a terminal funds_not_available failure. It is logged and never announced as successful.
Because payout idempotency is intentionally database-only:
- Stop the worker.
- Withdraw every remaining contract asset using the owner.
- Restore a backup, or migrate an empty database.
- Reindex from CONTRACT_DEPLOY_BLOCK.
- Let historical transfers become terminal underfunded disbursements.
- Resume the worker for new transfers.
Rebuilding without first draining the contract can repeat historical payments.
- The contract owner centrally controls membership, scores, accepted assets, minimums, the automator, pause state, and all withdrawals.
- The proxy administrator can replace the implementation.
- The off-chain worker calculates the 90 percent member distribution.
- The automator submits one payment instruction at a time.
- The contract does not enforce proportional correctness or database idempotency.
- Arbitrary tokens can be sent to the contract; unconfigured tokens are indexed but not distributed.
- Disabled, below-minimum, retained, or unsupported funds remain owner-controlled.
- Finalized block hashes are verified. A deep finalized reorganization halts processing for operator review.
- X posts are operational thank-you or warning messages, never token endorsements.