Predetermined Balances
Overview
Predetermined balances provide fine-grained control over the exact amounts and order of transfers in an approval. Unlike traditional tally-based systems where you approve a total amount (e.g., 100 badges) without controlling the specific combinations, predetermined balances let you explicitly define:
Exact amounts that must be transferred
Specific order of transfers
Precise badge IDs and ownership times for each transfer
Key Principle: The transfer will fail if the balances are not EXACTLY as defined in the predetermined balances.
Interface Definition
export interface PredeterminedBalances<T extends NumberType> {
manualBalances: ManualBalances<T>[];
incrementedBalances: IncrementedBalances<T>;
orderCalculationMethod: PredeterminedOrderCalculationMethod;
}
Balance Definition Methods
There are two mutually exclusive ways to define balances:
1. Manual Balances
Define an array of specific balance sets manually. Each element corresponds to a different transfer.
{
"manualBalances": [
{
"amount": "1",
"badgeIds": [
{
"start": "1",
"end": "1"
}
],
"ownershipTimes": [
{
"start": "1691978400000",
"end": "1723514400000"
}
]
},
{
"amount": "5",
"badgeIds": [
{
"start": "2",
"end": "6"
}
],
"ownershipTimes": [
{
"start": "1691978400000",
"end": "1723514400000"
}
]
}
]
}
Use Case: When you need complete control over each specific transfer amount and timing.
2. Incremented Balances
Define starting balances and rules for subsequent transfers. Perfect for sequential minting or time-based releases or other common patterns. Note that most options are incompatible with each other.
{
"incrementedBalances": {
"startBalances": [
{
"amount": "1",
"badgeIds": [
{
"start": "1",
"end": "1"
}
],
"ownershipTimes": [
{
"start": "1691978400000",
"end": "1723514400000"
}
]
}
],
"incrementBadgeIdsBy": "1",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"allowOverrideWithAnyValidBadge": false,
"recurringOwnershipTimes": {
"startTime": "0",
"intervalLength": "0",
"chargePeriodLength": "0"
}
}
}
Increment Options
incrementBadgeIdsBy
Amount to increment badge IDs by after each transfer
"1"
= next transfer gets badge ID 2, then 3, etc.
incrementOwnershipTimesBy
Amount to increment ownership times by
"86400000"
= add 1 day to ownership times
durationFromTimestamp
Calculate ownership times from timestamp + duration
"2592000000"
= 30 days from transfer time
allowOverrideTimestamp
Allow custom timestamp override in transfer
true
= users can specify custom start time
allowOverrideWithAnyValidBadge
Allow any valid badge ID (one) override
true
= users can specify any single valid badge ID
recurringOwnershipTimes
Define recurring time intervals
Monthly subscriptions, weekly rewards
Duration From Timestamp
Dynamically calculate ownership times from a timestamp plus a set duration. This overwrites all ownership times in the starting balances.
{
"durationFromTimestamp": "2592000000", // 30 days in milliseconds
"allowOverrideTimestamp": true
}
Behavior:
Default: Uses transfer time as the base timestamp
Override: If
allowOverrideTimestamp
is true, users can specify a custom timestamp inMsgTransferBadges
precalculationOptions
Calculation:
ownershipTime = baseTimestamp + durationFromTimestamp
Overwrite: All ownership times in starting balances are replaced with [{ "start": baseTimestamp, "end": baseTimestamp + durationFromTimestamp }]
Recurring Ownership Times
Define repeating time intervals for subscriptions or periodic rewards:
{
"recurringOwnershipTimes": {
"startTime": "1691978400000", // When intervals begin
"intervalLength": "2592000000", // 30 days in milliseconds
"chargePeriodLength": "604800000" // 7 days advance charging
}
}
Example: Monthly subscription starting August 13, 2023, with 7-day advance charging period.
Precalculating Balances
The Race Condition Problem
Predetermined balances can change rapidly between transaction broadcast and confirmation. For example:
Other users' mints get processed
Badge IDs shift due to concurrent activity
Manual balance specification becomes unreliable
The Solution: Precalculation
Use precalculateBalancesFromApproval
in MsgTransferBadges to dynamically calculate balances at execution time.
{
precalculateBalancesFromApproval: {
approvalId: string; // The approval to precalculate from
approvalLevel: string; // "collection" | "incoming" | "outgoing"
approverAddress: string; // "" if collection-level
version: string; // Must specify exact version
},
precalculationOptions: {
// Additional override options dependent on the selections
}
}
Order Calculation Methods
The system needs to determine which balance set to use for each transfer. This is controlled by the orderCalculationMethod
.
How Order Numbers Work
The order number determines which balances to transfer, but it works differently depending on the balance type:
Manual Balances
Order number = 0: Transfer
manualBalances[0]
(first element)Order number = 1: Transfer
manualBalances[1]
(second element)Order number = 5: Transfer
manualBalances[5]
(sixth element)
Example: If you have 3 manual balance sets, order numbers 0, 1, and 2 will use each set once. Order number 3 would be out of bounds.
Incremented Balances
Order number = 0: Use starting balances as-is (no increments)
Order number = 1: Apply increments once to starting balances
Order number = 5: Apply increments five times to starting balances
Example: Starting with badge ID 1, increment by 1:
Order 0: Badge ID 1
Order 1: Badge ID 2
Order 2: Badge ID 3
Order 5: Badge ID 6
Transfer-Based Order Numbers
Track the number of transfers to determine order:
useOverallNumTransfers
Global transfer count
Simple sequential transfers
usePerToAddressNumTransfers
Per-recipient count
User-specific limits
usePerFromAddressNumTransfers
Per-sender count
Sender-specific limits
usePerInitiatedByAddressNumTransfers
Per-initiator count
Initiator-specific limits
Important: Uses the same tracker as Max Number of Transfers. Trackers are:
Increment-only and immutable
Shared between predetermined balances and max transfer limits
Must be carefully managed to avoid conflicts
Merkle-Based Order Numbers
Use Merkle challenge leaf indices (leftmost = 0, rightmost = numLeaves - 1) for reserved transfers:
{
"useMerkleChallengeLeafIndex": true,
"challengeTrackerId": "uniqueId"
}
Use Case: Reserve specific badge IDs for specific users or claim codes.
Order Calculation Interface
export interface PredeterminedOrderCalculationMethod {
useOverallNumTransfers: boolean;
usePerToAddressNumTransfers: boolean;
usePerFromAddressNumTransfers: boolean;
usePerInitiatedByAddressNumTransfers: boolean;
useMerkleChallengeLeafIndex: boolean;
challengeTrackerId: string;
}
Boundary Handling
Understanding Bounds
Every approval defines bounds through its core fields (badgeIds, ownershipTimes, etc.). For example:
Badge IDs: 1-100
Ownership Times: Mon-Fri only
Transfer Times: Specific date range
Predetermined balances must work within these bounds, but note that order numbers can eventually exceed them.
Boundary Scenarios
Complete Out-of-Bounds
Scenario: Order number corresponds to balances completely outside approval bounds.
Example:
Approval allows badge IDs 1-100
Increment by 1 for each transfer
Order number 101 would require badge ID 101 (out of bounds)
Result: Transfer is ignored because badge ID 101 never matches the approval's badge ID range.
Partial Overlap
Scenario: Order number corresponds to balances that partially overlap with approval bounds.
Example:
Approval allows badge IDs 1-100
Transfer requires badge IDs 95-105
Badge IDs 95-100 are in bounds, 101-105 are out of bounds
Result:
Only in-bounds balances (95-100) are approved by current approval
Out-of-bounds balances (101-105) must be approved by a separate approval
The complete transfer (95-105) must still be exactly as defined
Important: The transfer will fail unless all out-of-bounds balances are approved by other approvals.
Last updated