|
| 1 | +# Transactional vs. Portfolio Contracts |
| 2 | + |
| 3 | +Agoric Orchestration supports both **transactional contracts** for single interactions and |
| 4 | +**portfolio contracts** for rich, persistent user positions. The [send-anywhere contract](./contract-walkthroughs/send-anywhere) |
| 5 | +is typical of transactional contracts: each offer is an independent interaction. |
| 6 | +[auto-stake-it](https://github.com/Agoric/agoric-sdk/blob/master/packages/orchestration/src/examples/auto-stake-it.contract.js) is typical of portfolio contracts: users have long-lived state. |
| 7 | +This section explains their differences and provides examples from real contracts. |
| 8 | + |
| 9 | +| **Characteristic** | **Transactional Contracts** | **Portfolio Contracts** | |
| 10 | +| :------------------------ | :---------------------------- | :------------------------------- | |
| 11 | +| **Interaction Type** | Single-shot | Multi-step, ongoing | |
| 12 | +| **Persistent User State** | No | Yes | |
| 13 | +| **User Account** | No dedicated on-chain account | Separate sub-account or position | |
| 14 | +| **Example Use Case** | Token swap, cross-chain send | Staking, vaults | |
| 15 | + |
| 16 | +Table: Comparison of Transactional and Portfolio Contracts in Agoric Orchestration |
| 17 | + |
| 18 | +## Transactional Contracts |
| 19 | + |
| 20 | +**Transactional contracts** perform one-shot actions, e.g., swapping tokens or sending them from one |
| 21 | +chain to another. Each use is typically triggered by a single transaction, after which there is no |
| 22 | +long-lived state managed by the contract for that user. |
| 23 | + |
| 24 | +**Characteristics**: |
| 25 | + |
| 26 | +- **Initiation**: Usually initiated by a single transaction from the user or external source. |
| 27 | +- **No per-user account**: The contract does not maintain persistent user-specific state. |
| 28 | +- **No smart wallet requirement**: Users often do not require a dedicated on-chain account. |
| 29 | +- **One-shot**: The operation typically starts and finishes in a single interaction. |
| 30 | + |
| 31 | +### Example: "Send Anywhere" Contract |
| 32 | + |
| 33 | +The `send-anywhere` contract illustrates how a user can send tokens from Agoric to another chain in |
| 34 | +one action. Under the hood, it exposes an `invitation` that, when exercised, moves tokens once and |
| 35 | +does not maintain any further user-specific state. |
| 36 | + |
| 37 | +**Key Points**: |
| 38 | + |
| 39 | +- The contract exposes a single `makeSendInvitation` method that returns an Invitation. |
| 40 | +- Users deposit tokens into the contract just for this one transaction. |
| 41 | +- After the tokens are sent, the seat concludes and there is no ongoing position to manage. |
| 42 | + |
| 43 | +Below is a simplified snippet adapted from the flows (`send-anywhere.flows.js`) showing the essence |
| 44 | +of the transactional flow: |
| 45 | + |
| 46 | +```js |
| 47 | +export const sendIt = async ( |
| 48 | + orch, |
| 49 | + { sharedLocalAccountP, log, zoeTools }, |
| 50 | + seat, |
| 51 | + { chainName, destAddr } |
| 52 | +) => { |
| 53 | + // 1. Extract the single token amount from the proposal. |
| 54 | + const { give } = seat.getProposal(); |
| 55 | + const { In: amount } = give; |
| 56 | + |
| 57 | + // 2. Transfer the tokens into a local account that can initiate the IBC transfer. |
| 58 | + await zoeTools.localTransfer(seat, sharedLocalAccountP, give); |
| 59 | + |
| 60 | + // 3. Execute the cross-chain transfer. |
| 61 | + const { chainId } = await orch.getChain(chainName); |
| 62 | + await sharedLocalAccountP.transfer( |
| 63 | + { value: destAddr, chainId }, |
| 64 | + { denom, value: amount.value } |
| 65 | + ); |
| 66 | + |
| 67 | + // 4. Finish. No persistent state is stored for this user. |
| 68 | + seat.exit(); |
| 69 | +}; |
| 70 | +``` |
| 71 | + |
| 72 | +In this snippet, we perform the following steps: |
| 73 | + |
| 74 | +1. **Extract token Details**: The user's seat provides the tokens details (_amount_, i.e., _brand_ and _value_) in a single `give`. |
| 75 | +2. **Send via localAccount**: An account on the local Agoric chain is used only as a helper to transfer funds from `seat` to an ICA account. |
| 76 | +3. **IBC transfer**: Tokens are transferred cross-chain in a single step via [`transfer` API](/guides/orchestration/key-concepts#funds-transfer). |
| 77 | +4. **No ongoing portfolio**: Once done, the seat concludes—there is no stored user state. |
| 78 | + |
| 79 | +This is a typical example of a transactional contract. It performs a single |
| 80 | +action (sending tokens) and does not maintain any ongoing user state. The user interacts once, and |
| 81 | +the contract concludes the operation. This contract is discussed in more detail in the |
| 82 | +[`send-anywhere` contract walkthrough](/guides/orchestration/contract-walkthroughs/send-anywhere). |
| 83 | + |
| 84 | +## Portfolio Contracts |
| 85 | + |
| 86 | +**Portfolio contracts** manage long-lived user state and support multiple interactions over time. |
| 87 | +They resemble "vaults": you open a portfolio (or vault) position, then manipulate or monitor it as |
| 88 | +desired. |
| 89 | + |
| 90 | +**Characteristics**: |
| 91 | + |
| 92 | +- **Persistent per-user state**: Each user typically has their own account. |
| 93 | +- **Multiple user actions**: The state can be updated with each new user action. |
| 94 | +- **Orchestration Flows**: Users often interact through orchestration flows. |
| 95 | +- **Onboarding**: Users first deposit tokens into a personal portfolio for management. |
| 96 | + |
| 97 | +### Example: "Auto Stake It" Contract |
| 98 | + |
| 99 | +The `auto-stake-it` contract demonstrates how to continuously manage a user's positions. A user |
| 100 | +"onboards" into the contract by creating accounts on multiple chains (via orchestration) and then |
| 101 | +depositing tokens to be staked automatically. |
| 102 | + |
| 103 | +Key points in code: |
| 104 | + |
| 105 | +1. **Make a PortfolioHolder** that stores each user's accounts in a single structure. |
| 106 | +2. **Add accounts** to the portfolio. Each user might have multiple chain accounts. |
| 107 | +3. **Perform multi-step operations** (e.g. receive tokens over IBC, transfer to staking). |
| 108 | + |
| 109 | +Below is a simplified snippet (`auto-stake-it.flows.js`) showing how a portfolio is set up when a |
| 110 | +user calls the "makeAccounts" flow: |
| 111 | + |
| 112 | +```js |
| 113 | +export const makeAccounts = async (orch, { makeStakingTap, makePortfolioHolder, chainHub }, |
| 114 | + seat, { chainName, validator }) => { |
| 115 | + seat.exit(); // no funds are exchanged at the moment |
| 116 | + |
| 117 | + // 1. Get chain references (e.g., 'agoric' chain and the remote chain). |
| 118 | + const [agoric, remoteChain] = await Promise.all([ |
| 119 | + orch.getChain('agoric'), |
| 120 | + orch.getChain(chainName), |
| 121 | + ]); |
| 122 | + |
| 123 | + // 2. Create orchestration accounts for each chain. |
| 124 | + const [localAccount, stakingAccount] = await Promise.all([ |
| 125 | + agoric.makeAccount(), |
| 126 | + remoteChain.makeAccount(), |
| 127 | + ]); |
| 128 | + |
| 129 | + // 3. Combine them into a single PortfolioHolder for the user. |
| 130 | + const accountEntries = harden([ |
| 131 | + ['agoric', localAccount], |
| 132 | + [chainName, stakingAccount], |
| 133 | + ]); |
| 134 | + const publicTopicEntries = /* gather each account's public topics */; |
| 135 | + const portfolioHolder = makePortfolioHolder(accountEntries, publicTopicEntries); |
| 136 | + |
| 137 | + // 4. Return a continuing offer result with invitationMakers, etc. |
| 138 | + return portfolioHolder.asContinuingOffer(); |
| 139 | +}; |
| 140 | +``` |
| 141 | + |
| 142 | +In this snippet, we create multiple accounts (one for Agoric, one for the remote chain). |
| 143 | +Then, the contract uses `makePortfolioHolder` to maintain these in a persistent store. |
| 144 | +Users can perform multiple future actions without losing state. Learn more about the |
| 145 | +`makePortfolioHolder` function in the [`makePortfolioHolder` Orchestration API reference](https://agoric-sdk.pages.dev/funcs/_agoric_orchestration.preparePortfolioHolder). |
| 146 | + |
| 147 | +## Choosing Between Transactional and Portfolio |
| 148 | + |
| 149 | +Use **transactional contracts** (like "send-anywhere") when: |
| 150 | + |
| 151 | +- You want a **single-shot interaction** (e.g. a swap, a cross-chain send). |
| 152 | +- No ongoing user state is needed. |
| 153 | +- The user wants minimal overhead (no dedicated on-chain structure). |
| 154 | + |
| 155 | +Use **portfolio contracts** (like "auto-stake-it") when: |
| 156 | + |
| 157 | +- You need **long-lived, multi-step interactions** (e.g. staking, compounding). |
| 158 | +- Each user needs a separate sub-account or position. |
| 159 | +- User state must persist (e.g., "my tokens remain staked until I withdraw"). |
| 160 | + |
| 161 | +Agoric Orchestration supports both **transactional contracts** for single interactions and |
| 162 | +**portfolio contracts** for rich, persistent user positions. Use the pattern that best fits |
| 163 | +your user flows. A purely transactional workflow is great for quick, one-time trades, while |
| 164 | +a portfolio-based approach is ideal for scenarios like vaults or staking, where users |
| 165 | +maintain ongoing positions. Note that rather than a rigid dichotomy, many contracts exist on a |
| 166 | +spectrum supporting both interaction types. For instance, the [FastUSDC contract](https://github.com/Agoric/agoric-sdk/tree/master/packages/fast-usdc) supports both |
| 167 | +portfolio-style LP positions and transactional trades without persistent user state. |
0 commit comments