DeFi position integrations for Trackall. The package currently contains:
- Solana integrations under
src/integrations/solana/* - Movement integrations under
src/integrations/movement/*, using the Aptos SDK integration shape - Shared platform metadata in
src/platforms/* - Shared token plugins and test harnesses in
src/plugin/*andsrc/test/*
The package entrypoint currently auto-discovers and exports Solana integrations. Movement integrations are implemented and tested in-repo, but are not auto-exported from the package root.
Integrations return UserDefiPosition[] built from the shared types in src/types/position.ts.
Current top-level positionKind values:
lendingfor supplied and borrowed credit positionsstakingfor assets actively staked or in unbondingliquidityfor AMM or concentrated-liquidity pool positionsvestingfor locked token allocations that unlock over timerewardfor standalone claimable distributions such as airdrops or unclaimed incentives
Modeling rules:
- Use the dedicated top-level kind when the position itself is fundamentally lending, staking, liquidity, vesting, or reward-based.
- Use the shared
rewardsfield on a non-reward position only for incentives attached to that primary position. - Use
rewardwhen the claimable asset stands alone and is not naturally attached to a lending, staking, liquidity, or vesting position.
All shared position types extend BaseDefiPosition, which supports optional integration-specific metadata:
type PositionMetadata = Record<string, Record<string, unknown>>Use meta only for structured protocol-specific details that do not fit an existing shared field. Prefer canonical shared fields such as positionKind, rewards, usdValue, token amounts, and timestamps whenever the concept already has a dedicated place in the schema.
Keep metadata keys stable and semantic so downstream consumers can rely on them. Prefer domain nouns like subaccount, vault, or lock, with an object payload under each key.
Example:
meta: {
subaccount: {
name: 'Hedge',
},
}src/
integrations/
solana/<integration-name>/
index.ts
index.test.ts
movement/<integration-name>/
index.ts
index.test.ts
platforms/
<integration-name>.ts
index.ts
plugin/
solana/
aptos/
test/
solana-integration.ts
aptos-integration.ts
run-integration.test.ts
Add src/platforms/<integration-name>.ts with the Platform shape:
import type { Platform } from '../types/platform'
const myPlatform = {
id: 'my-platform' as const,
networks: ['movement'],
name: 'My Platform',
location: {
latitude: 1.3521,
longitude: 103.8198,
},
image: 'https://example.com/logo.png',
description: 'Short description',
tags: ['lending'],
defiLlamaId: 'my-platform',
links: {
website: 'https://example.com',
documentation: 'https://docs.example.com',
},
} satisfies Platform
export default myPlatformRegister it in src/platforms/index.ts so its id becomes part of the PlatformId union.
Pick the network-specific path:
- Solana:
src/integrations/solana/<integration-name>/ - Movement:
src/integrations/movement/<integration-name>/
Each integration should have:
index.tsfor the integration implementationindex.test.tsfor the network-specific test entrypoint when local integration tests are needed
Export testAddress from index.ts whenever the generic test runner should exercise getUserPositions.
Implement SolanaIntegration from src/types/solanaIntegration.ts.
import type { SolanaIntegration } from '../../../types/solanaIntegration'
export const testAddress = 'WalletAddressWithPositions'
const integration: SolanaIntegration = {
platformId: 'my-platform',
getUserPositions(address, plugins) {
return (async function* () {
const accounts = yield ['AccountPubkey1', 'AccountPubkey2']
const programAccounts = yield {
kind: 'getProgramAccounts' as const,
programId: 'ProgramId',
filters: [],
}
return []
})()
},
}
export default integrationSolana integrations use the async-generator batching protocol:
yield string[]to request specific accountsyield { kind: 'getProgramAccounts', ... }to request program-owned accountsyield { kind: 'getTokenAccountsByOwner', ... }when token-account discovery is neededreturn UserDefiPosition[]when the plan is complete
Use runIntegrations, fetchAccountsBatch, and fetchProgramAccountsBatch for tests and local execution. Do not make direct RPC calls inside the integration when the same data can be requested through yielded batch operations.
Every Solana integration must export a top-level PROGRAM_IDS constant from the module. Do not put indexedPrograms on the SolanaIntegration object; the package root aggregates exported PROGRAM_IDS constants automatically, and CI treats missing or invalid PROGRAM_IDS as a failure.
When building positions, prefer the shared types from src/types/position.ts:
LendingDefiPositionStakingDefiPositionLiquidityDefiPositionVestingDefiPositionRewardDefiPosition
Use meta for extra structured protocol details that are useful to consumers but do not belong in the shared schema, for example meta.subaccount.name for Drift subaccounts.
Implement AptosIntegration from src/types/aptosIntegration.ts.
import type {
AptosIntegration,
AptosPlugins,
} from '../../../types/aptosIntegration'
import type { UserDefiPosition } from '../../../types/position'
export const testAddress = '0xwallet_with_known_positions'
async function getUserPositions(
address: string,
{ client, tokens }: AptosPlugins,
): Promise<UserDefiPosition[]> {
const token = await tokens.fetch('0x1::aptos_coin::AptosCoin')
void address
void client
void token
return []
}
export const myIntegration: AptosIntegration = {
platformId: 'my-platform',
getUserPositions,
}
export default myIntegrationMovement integrations in this repo currently use:
AptosPluginswithclientandAptosTokenPlugin- direct async calls rather than the Solana generator protocol
testAptosIntegrationfromsrc/test/aptos-integration.tsfor shared test coverage
Use src/integrations/movement/yuzu/index.ts as the canonical example.
Use the same shared position taxonomy here:
- emit
vestingfor token unlock schedules - emit
rewardfor standalone claimable airdrops or incentive balances - keep nested
rewardsfor incentives attached to another primary position - use
metafor structured protocol-specific details that do not fit shared fields, for examplemeta.subaccount.name
Create src/integrations/solana/<integration-name>/index.test.ts and wire it through the shared Solana harness:
import { testIntegration } from '../../../test/solana-integration'
import { myIntegration, testAddress } from '.'
testIntegration(myIntegration, testAddress)For richer Solana-specific batching assertions, expand the test like src/integrations/solana/meteora/index.test.ts.
Create src/integrations/movement/<integration-name>/index.test.ts:
import { testAptosIntegration } from '../../../test/aptos-integration'
import { myIntegration, testAddress } from '.'
testAptosIntegration(myIntegration, testAddress)Install dependencies:
bun installType-check:
bun run typecheckRun a Solana integration test:
SOLANA_RPC_URL=https://... bun test src/integrations/solana/meteora/index.test.tsRun a Movement integration test:
MOVEMENT_RPC_URL=https://mainnet.movementnetwork.xyz/v1 \
MOVEMENT_INDEXER_URL=https://indexer.mainnet.movementnetwork.xyz/v1/graphql \
bun test src/integrations/movement/yuzu/index.test.tsRun the generic integration test:
INTEGRATION_NAME=yuzu \
INTEGRATION_NETWORK=movement \
MOVEMENT_RPC_URL=https://mainnet.movementnetwork.xyz/v1 \
MOVEMENT_INDEXER_URL=https://indexer.mainnet.movementnetwork.xyz/v1/graphql \
bun test src/test/run-integration.test.tsIf INTEGRATION_NETWORK is omitted, the generic runner will try to infer it by scanning src/integrations/*.
CI is defined in .github/workflows/ci.yml.
Current behavior:
- CI installs dependencies and runs
bun run typecheck. - It scans
git difffor the first changed path matchingsrc/integrations/<network>/<name>/.... - If
src/integrations/<network>/<name>/index.test.tsexists, CI runs that file as the local integration test. - CI also runs
src/test/run-integration.test.tswithINTEGRATION_NAME=<name>andINTEGRATION_NETWORK=<network>. - For Movement integrations, CI passes
MOVEMENT_RPC_URLandMOVEMENT_INDEXER_URL. - For Solana integrations, CI passes
SOLANA_RPC_URL.
Practical implications:
- Keep one integration per PR when possible, because CI detects the first changed integration path.
- Export
testAddresswhengetUserPositionsshould be exercised by the shared test harness. - Put local integration tests at
src/integrations/<network>/<name>/index.test.ts.
This package uses Biome.
# Check formatting and lint rules
bun run lint
# Auto-format files
bun run format| Purpose | File |
|---|---|
| Solana integration interface | src/types/solanaIntegration.ts |
| Aptos/Movement integration interface | src/types/aptosIntegration.ts |
| Shared platform registry | src/platforms/index.ts |
| Solana token plugin | src/plugin/solana/tokens.ts |
| Aptos token plugin | src/plugin/aptos/tokens.ts |
| Solana shared test harness | src/test/solana-integration.ts |
| Aptos shared test harness | src/test/aptos-integration.ts |
| Generic integration runner | src/test/run-integration.test.ts |