02 · 7 min read
Create and claim
What happens between a sender clicking Create gift link and a recipient holding the tokens: the link itself, the two transactions that create it, the three ways to receive, and how an unclaimed link comes home.
01Anatomy of a link#
https://yield-links-hedera.vercel.app/claim#k=0x<64 hex characters>&c=296| Part | Meaning |
|---|---|
k | The private half of a throwaway key made in the sender's browser. Its address is the link's linkKey on chain. |
c | The chain id: 296 is Hedera testnet, 295 is mainnet. |
# | Everything after it is the fragment. Browsers never send the fragment to a server, so the key never reaches the app, the relayer or a log. |
The key is single use, forever. Links are never deleted, only moved to Claimed or Refunded, so an old signature can never be replayed against a new link that reuses a key.
02Creating a link#
The sender signs two transactions:
- Approve the source, not
YieldLinks, foramount + ROUNDING_DUST. The source pulls the tokens, because it is the only contract that ever holds them. createLink(linkKey, token, amount, expiry, policy)with 1.5 HBAR attached as the prepaid network fee.
import { newKeypair, buildClaimUrl } from "~~/utils/yieldlinks";
const { privateKey, address: linkKey } = newKeypair();
// approve(source, amount + 10n), then:
await writeLinks({
functionName: "createLink",
args: [linkKey, SAUCE, amount, expiry, policy],
value: parseEther("1.5"),
});
const url = buildClaimUrl(window.location.origin, privateKey, chainId);Inside the call the source stakes the SAUCE in SaucerSwap's Infinity Pool (enter, SAUCE to xSAUCE) and YieldLinks mints the link its shares of the pool. Expiry must be in the future and at most 365 days away; the UI offers 1, 7 or 30 days.
03Who keeps the yield#
Chosen per link, and fixed once it is created.
| Policy | Recipient gets | Yield goes to |
|---|---|---|
Recipient | Principal plus yield | The recipient. The gift grows for them. |
Sender | The principal | The sender, paid out in the same claim |
Charity | The principal | The CHARITY address set at deploy (CHARITY_ADDRESS, defaults to the deployer) |
04Three ways to receive#
| Option | For | What happens |
|---|---|---|
| New wallet | Someone with nothing | The browser generates an ED25519 key. The relayer creates the account for it (0.1 HBAR welcome balance, unlimited token auto-association), then the claim pays it. |
| My wallet | Anyone with a wallet, including MetaMask | Connect, and the claim pays the connected address. |
| Address | Paying someone else | Paste any EVM address. It need not exist on Hedera yet: the HIP-904 airdrop creates it. |
A generated wallet is shown once, in two forms: the 96-character DER key (302e…) that HashPack imports, and the 64-character raw key. It works in HashPack, Blade and Hedera tools, but not MetaMask, which only takes ECDSA keys. Anyone who wants MetaMask should choose My wallet.
05The relayer submits it#
The claim page never sends the private key anywhere. It signs a claim and posts the signature:
// EIP-712, domain { name: "YieldLinks", version: "1", chainId, verifyingContract }
// type Claim(address linkKey, address recipient)
const signature = await signClaim(privateKey, contract, chainId, recipient);
await fetch("/api/claim", {
method: "POST",
body: JSON.stringify({ chainId, linkKey, recipient, signature }),
});/api/claim checks the link prepaid at least 1.35 HBAR, simulates the call, and only then sends claim. A bad claim costs the relayer nothing. On chain the contract recovers the signer, which must equal linkKey, so a watcher who swaps in their own recipient breaks the signature.
The claim prices the link's shares, unstakes from SaucerSwap, and pays through a HIP-904 airdrop, which creates and associates the recipient's account if needed. The prepaid fee goes to whoever submitted it.
06Cancel and refund#
| When | Who may call refund | Where it goes |
|---|---|---|
| Before expiry | Only the sender | Principal, yield and the prepaid fee, back to the sender |
| After expiry | Anyone | Still only the sender |
Nothing needs a keeper or a schedule: an expired link just waits until someone calls refund. The sender cancels from My links.
07Life of a link#
None ──createLink──▶ Open ──claim (valid signature, before expiry)──▶ Claimed
│
└──refund (sender any time, anyone after expiry)──▶ RefundedClaimed and Refunded are final. The link's shares are burned and its fee is zeroed in the same step, before any tokens move.