atfi
v1.1.4
Published
TypeScript SDK for ATFi commitment vaults on Base
Maintainers
Readme
ATFi SDK
TypeScript SDK for interacting with ATFi commitment vaults on Base. Stake tokens, verify attendance, earn rewards.
Features
- Simulation-first - Know results before wallet popup
- Transaction callbacks - Loading states for UX
- Read-only mode - Show data before wallet connection
- User-centric queries - Dashboard, registered events, claimable rewards
- Real-time watching - Subscribe to live vault events
- TypeScript - Full type safety
Installation
npm install atfi viemQuick Start
import { ATFiSDK } from 'atfi';
import { createPublicClient, createWalletClient, http, custom } from 'viem';
import { base } from 'viem/chains';
// Setup clients
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
const walletClient = createWalletClient({
chain: base,
transport: custom(window.ethereum),
});
// Initialize SDK
const sdk = new ATFiSDK(publicClient, walletClient);How It Works
Every write function returns { simulation, execute() } - one call gives you everything:
const action = await sdk.register({ vaultAddress: '0x...' });
// 1. Check simulation result (NO wallet popup yet!)
if (!action.simulation.success) {
console.error(action.simulation.error.message);
return;
}
// 2. Show user what will happen
console.log(`Stake: ${action.simulation.stakeAmount} ${action.simulation.tokenSymbol}`);
console.log(`Your balance: ${action.simulation.userBalance}`);
console.log(`Gas estimate: ${action.simulation.gasEstimate}`);
// 3. User confirms → execute with loading callbacks
const result = await action.execute({
onApproving: () => setStatus('Approving token...'),
onApproved: (hash) => setStatus('Token approved!'),
onSubmitting: () => setStatus('Submitting...'),
onSubmitted: (hash) => setStatus(`TX: ${hash}`),
onConfirming: () => setStatus('Confirming...'),
});
console.log('Done!', result.txHash);Read-Only Mode
Show data before user connects wallet:
import { ATFiSDK } from 'atfi';
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';
// No wallet needed!
const publicClient = createPublicClient({
chain: base,
transport: http(),
});
const sdk = ATFiSDK.readOnly(publicClient);
// All read functions work
const events = await sdk.getAllEvents();
const info = await sdk.getEventInfo('0x...');
// Write functions throw helpful error
await sdk.register({ vaultAddress: '0x...' });
// Error: "SDK is in read-only mode. Initialize with a walletClient to perform write operations."API Reference
Create Event (Organizer)
const action = await sdk.createEvent({
stakeAmount: '10', // 10 USDC
maxParticipants: 50,
useYield: true, // Enable Morpho yield
token: 'USDC', // 'USDC' or 'IDRX'
});
if (action.simulation.success) {
console.log('Expected vault ID:', action.simulation.expectedVaultId);
console.log('Token:', action.simulation.token.symbol);
console.log('Gas:', action.simulation.gasEstimate);
const result = await action.execute({
onSubmitting: () => showLoader('Creating event...'),
onConfirming: () => showLoader('Confirming...'),
});
console.log('Created:', result.vaultAddress);
}Simulation returns:
| Field | Type | Description |
|-------|------|-------------|
| expectedVaultId | bigint | Next vault ID |
| token | object | { address, symbol, decimals } |
| stakeAmount | string | Human readable stake |
| maxParticipants | number | Max allowed |
| useYield | boolean | Yield enabled |
| gasEstimate | bigint | Estimated gas |
Register (Participant)
const action = await sdk.register({ vaultAddress: '0x...' });
if (action.simulation.success) {
console.log(`Stake: ${action.simulation.stakeAmount} ${action.simulation.tokenSymbol}`);
console.log(`Your balance: ${action.simulation.userBalance}`);
console.log(`Needs approval: ${action.simulation.needsApproval}`);
console.log(`Spots left: ${action.simulation.maxParticipants - action.simulation.currentParticipants}`);
// Callbacks for loading states (approval + stake transaction)
const result = await action.execute({
onApproving: () => showLoader('Approving USDC...'),
onApproved: (hash) => showToast(`Approved! ${hash}`),
onSubmitting: () => showLoader('Staking...'),
onConfirming: () => showLoader('Confirming...'),
});
}Simulation returns:
| Field | Type | Description |
|-------|------|-------------|
| stakeAmount | string | Amount to stake |
| tokenSymbol | string | USDC or IDRX |
| userBalance | string | Your token balance |
| currentAllowance | string | Current approval |
| needsApproval | boolean | Need approve TX first |
| currentParticipants | number | Current count |
| maxParticipants | number | Max allowed |
Start Event (Organizer)
const action = await sdk.startEvent({ vaultAddress: '0x...' });
if (action.simulation.success) {
console.log(`Total staked: ${action.simulation.totalStaked}`);
console.log(`Participants: ${action.simulation.participantCount}`);
console.log(`Has yield: ${action.simulation.hasYield}`);
const result = await action.execute();
}Verify Participants (Organizer)
const action = await sdk.verifyParticipant({
vaultAddress: '0x...',
participants: ['0xAddr1', '0xAddr2', '0xAddr3'],
});
if (action.simulation.success) {
console.log(`Will verify: ${action.simulation.toVerify.length}`);
console.log(`Already verified: ${action.simulation.alreadyVerified.length}`);
console.log(`Not staked: ${action.simulation.notStaked.length}`);
const result = await action.execute();
}Simulation returns:
| Field | Type | Description |
|-------|------|-------------|
| toVerify | Address[] | Will be verified |
| alreadyVerified | Address[] | Already done |
| notStaked | Address[] | Not registered (will fail) |
| currentVerified | number | Current count |
| newVerifiedCount | number | Count after |
Settle Event (Organizer)
const action = await sdk.settleEvent({ vaultAddress: '0x...' });
if (action.simulation.success) {
console.log(`Verified: ${action.simulation.verifiedCount}`);
console.log(`No-shows: ${action.simulation.noShowCount}`);
console.log(`Yield earned: ${action.simulation.estimatedYield}`);
console.log(`Protocol fees: ${action.simulation.estimatedProtocolFees}`);
console.log(`Reward per attendee: ${action.simulation.estimatedRewardPerParticipant}`);
const result = await action.execute();
}Simulation returns:
| Field | Type | Description |
|-------|------|-------------|
| totalStaked | string | Total staked |
| verifiedCount | number | Attendees |
| noShowCount | number | No-shows |
| estimatedYield | string | Yield earned |
| estimatedProtocolFees | string | Protocol fees |
| estimatedNoShowFees | string | No-show fees |
| estimatedRewardPerParticipant | string | Reward each |
Claim Rewards (Participant)
const action = await sdk.claim({ vaultAddress: '0x...' });
if (action.simulation.success) {
console.log(`Total: ${action.simulation.claimableAmount} ${action.simulation.tokenSymbol}`);
console.log(`Your stake: ${action.simulation.stakeAmount}`);
console.log(`Bonus: ${action.simulation.bonusShare}`);
const result = await action.execute();
}Simulation returns:
| Field | Type | Description |
|-------|------|-------------|
| claimableAmount | string | Total to claim |
| tokenSymbol | string | Token symbol |
| stakeAmount | string | Original stake |
| bonusShare | string | Bonus from no-shows + yield |
Read Functions
// Get event details
const info = await sdk.getEventInfo('0xVaultAddress');
// Get participant status
const status = await sdk.getParticipantStatus('0xVault', '0xParticipant');
// Get all events (batched for performance)
const events = await sdk.getAllEvents();
// Get vault by ID
const vault = await sdk.getVaultAddress(1);Token Balance Helpers
// Get single token balance
const usdcBalance = await sdk.getTokenBalance('0xUserAddress', 'USDC');
console.log(`USDC: ${usdcBalance}`); // "125.50"
// Get all supported token balances
const balances = await sdk.getAllTokenBalances('0xUserAddress');
console.log(balances);
// { USDC: "125.50", IDRX: "1000000.00" }User-Centric Queries
Get events filtered by user:
// Events created by user
const myEvents = await sdk.getEventsByOwner('0xUserAddress');
// Events user registered for
const registered = await sdk.getRegisteredEvents('0xUserAddress');
// Events with unclaimed rewards
const claimable = await sdk.getClaimableEvents('0xUserAddress');
// Complete dashboard data (all in one call)
const dashboard = await sdk.getUserDashboard('0xUserAddress');
console.log(dashboard);
// {
// asOrganizer: [...], // Events user created
// asParticipant: [...], // Events user joined
// claimable: [...], // Ready to claim
// balances: { USDC: "125.50", IDRX: "1000000.00" }
// }Transaction Callbacks
All write functions accept callbacks for loading states:
interface TransactionCallbacks {
onApproving?: () => void; // Starting token approval
onApproved?: (txHash: Hash) => void; // Approval submitted
onSubmitting?: () => void; // Starting main transaction
onSubmitted?: (txHash: Hash) => void; // Transaction submitted
onConfirming?: () => void; // Waiting for confirmation
}
// Example: Full loading state management
const result = await action.execute({
onApproving: () => {
setStatus('approving');
setMessage('Please approve token in wallet...');
},
onApproved: (hash) => {
setMessage(`Approved! TX: ${hash.slice(0, 10)}...`);
},
onSubmitting: () => {
setStatus('submitting');
setMessage('Please confirm transaction in wallet...');
},
onSubmitted: (hash) => {
setMessage(`Submitted! TX: ${hash.slice(0, 10)}...`);
setTxHash(hash);
},
onConfirming: () => {
setStatus('confirming');
setMessage('Waiting for blockchain confirmation...');
},
});
setStatus('success');Real-Time Event Watching
Subscribe to vault events for live UI updates:
const unwatch = sdk.watchVault('0xVaultAddress', {
onParticipantJoined: (address, total) => {
console.log(`${address} joined! Total: ${total}`);
updateParticipantList();
},
onParticipantVerified: (address, totalVerified) => {
console.log(`${address} verified!`);
updateVerifiedCount(totalVerified);
},
onSettled: (totalYield, protocolFee) => {
console.log(`Event settled! Yield: ${totalYield}`);
showClaimButton();
},
onClaimed: (address, amount) => {
console.log(`${address} claimed ${amount}`);
},
onStakingStatusChanged: (isOpen) => {
updateRegistrationStatus(isOpen);
},
onError: (error) => {
console.error('Watch error:', error);
},
});
// Stop watching when done
unwatch();Error Handling
Errors are returned in simulation, not thrown:
const action = await sdk.register({ vaultAddress: '0x...' });
if (!action.simulation.success) {
switch (action.simulation.error.code) {
case 'STAKING_CLOSED':
showMessage('Registration ended');
break;
case 'ALREADY_STAKED':
showMessage('Already registered');
break;
case 'INSUFFICIENT_BALANCE':
showMessage(`Need ${action.simulation.stakeAmount} ${action.simulation.tokenSymbol}`);
break;
case 'MAX_PARTICIPANTS_REACHED':
showMessage('Event is full');
break;
default:
showMessage(action.simulation.error.message);
}
return;
}Error Codes:
STAKING_CLOSED- Registration endedALREADY_STAKED- Already registeredINSUFFICIENT_BALANCE- Not enough tokensMAX_PARTICIPANTS_REACHED- Event fullNOT_OWNER- Not event ownerEVENT_ALREADY_STARTED- Already startedVAULT_ALREADY_SETTLED- Already settledVAULT_NOT_SETTLED- Not settled yetNOT_VERIFIED- Not verified for attendanceALREADY_CLAIMED- Already claimed
Supported Tokens
| Token | Address | Decimals | Yield |
|-------|---------|----------|-------|
| USDC | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 6 | Yes |
| IDRX | 0x18Bc5bcC660cf2B9cE3cd51a404aFe1a0cBD3C22 | 2 | No |
Contract Addresses (Base Mainnet)
| Contract | Address |
|----------|---------|
| FactoryATFi | 0x0be05a5fa7116c1b33f2b0036eb0d9690db9075f |
| Morpho Vault | 0x050cE30b927Da55177A4914EC73480238BAD56f0 |
License
MIT
