Target Savings
A goal saves toward a target by a deadline. Scheduled deposits are collected by the contract, and the user can also deposit by hand.
Create a goal
createGoal creates a goal for publicKey and gives you its id. Ids are not sequential.
import { toBaseUnits } from '@loktin/sdk';
const goal = await targetSavings.createGoal({
target: toBaseUnits('500'),
deadline: new Date('2027-06-30T00:00:00Z'),
cadenceSeconds: 7 * 24 * 60 * 60,
depositSize: toBaseUnits('20'),
rule: { kind: 'penalty', bps: 500 },
});
console.log('New goal id:', goal.result);
if (goal.allowance?.renewBy !== undefined) {
console.log('Renew the allowance by', goal.allowance.renewBy);
}
// Later, before `renewBy`:
await targetSavings.renewAllowance(goal.result);Withdrawal rules
Each goal has one rule, chosen when it is created:
- Penalty: the user may withdraw early and pays a penalty of up to 1,000 basis points (10%). The penalty goes to the integrator.
- Lock: there is no withdrawal until the target is reached or the deadline passes.
Once the target is reached, or from the deadline second onwards, a withdrawal carries no penalty. Fees still apply.
Amounts
Amounts are bigint base units. USDC has 7 decimals. toBaseUnits and fromBaseUnits convert to and from decimal strings.
The allowance
An owner has one token allowance for Target Savings, shared by all their goals. Each approval replaces it whole.
createGoal takes an allowance option:
'auto'is the default. The SDK approves what the scheduled deposits of all the owner's open goals need, the new one included.'none'approves nothing and leaves the current allowance as it is.{ amount, expiryLedger }approves exactly that, and replaces the allowance of all the owner's goals.
Create an owner's goals one at a time, so each approval sees the goals before it.
The allowance covers the deposits due before it expires. It expires one day after the latest deadline of those goals, unless the network's longest allowed time is shorter. In that case the result has a renewBy date, 7 days before the allowance expires.
Call renewAllowance before that date, or later scheduled deposits fail for lack of allowance. It covers all the owner's open goals, as 'auto' does.
Manual deposits
deposit moves an amount from publicKey into their goal.
await targetSavings.deposit(goal.result, toBaseUnits('5'));Check before signing
creationBreakdown shows what an early withdrawal of the whole target would pay out, before the user signs. It makes no network call.
import { fromBaseUnits } from '@loktin/sdk';
import { creationBreakdown } from '@loktin/sdk/target-savings';
const breakdown = creationBreakdown({
target: toBaseUnits('500'),
rule: { kind: 'penalty', bps: 500 },
loktinFeeBps: await targetSavings.loktinFeeBps(),
integratorFeeBps: targetSavings.integratorFeeBps,
});
console.log('An early withdrawal pays', fromBaseUnits(breakdown.toUser), 'USDC');allowanceStatus says whether the next scheduled deposit would be short on allowance. The other goals' collections use the same allowance. It does not check the owner's balance.
const status = await targetSavings.allowanceStatus(goal.result);
if (status.short) {
await targetSavings.renewAllowance(goal.result);
}Find goals
There is no paging by number. Use the id that createGoal gives you. To find goals, do one of these:
- Call
userGoalsfor each of your own users, at most 20 goals each. - Read the contract's
goal_createdandgoal_closedevents into your own store.
const goals = await targetSavings.userGoals();
for (const open of goals) {
console.log(open.id, fromBaseUnits(open.principal), 'USDC');
}Withdraw
withdraw withdraws the whole goal and closes it. previewWithdraw shows how a withdrawal would split right now.
const split = await targetSavings.previewWithdraw(goal.result);
console.log('The user receives', fromBaseUnits(split.toUser), 'USDC');
const withdrawal = await targetSavings.withdraw(goal.result);
console.log('Paid', fromBaseUnits(withdrawal.result.toUser), 'USDC');The user receives toUser. The penalty and the integrator fee accrue to the integrator, and the Loktin fee to Loktin's fee recipient. See Security and trust.
Under a lock rule, a withdrawal before the target or deadline is refused while the goal holds any principal. A goal with nothing deposited can be closed at any time, under any rule.
Errors
Every error the SDK throws is a LoktinError with a stable code:
| Code | Meaning |
|---|---|
INVALID_INPUT | An input was refused before any network call. |
CONTRACT_ERROR | Target Savings or Spend & Save refused the call with one of its documented codes. |
TOKEN_ERROR | The token contract refused the call, for example for a low balance or a missing trustline. |
UNKNOWN_CONTRACT_ERROR | A contract error the SDK cannot name. |
SIGNER_REFUSED | The wallet refused to sign, or failed while signing. |
NETWORK | The RPC could not be reached, or answered with an error, after the SDK's retries: 3 for a read, and 3 for each step of a write before signing. |
PENDING | The transaction was sent and its outcome is not known yet. Look it up by hash; do not resend. |
TRANSACTION_FAILED | The transaction was sent and failed. |
Each error is reported against the contract that raised it. The contract codes are listed in docs/errors.md.
import { ContractError, LoktinError, PendingError } from '@loktin/sdk';
try {
await targetSavings.deposit(goal.result, toBaseUnits('5'));
} catch (error) {
if (error instanceof ContractError) {
console.log(error.errorName, error.description);
} else if (error instanceof PendingError) {
// The outcome is not known yet. Look it up by hash; do not send again.
console.log(error.hash, error.validUntil);
} else if (error instanceof LoktinError) {
console.log(error.code, error.message);
} else {
throw error;
}
}