Skip to main content

Introduction

Hydra is a wallet of wallets, a fanout wallet if you will. It enables extremely large membership sets that can take part in fund distribution from a central wallet. It works with SOL and any SPL token.

🔗 Helpful links:

danger

The PROGRAM ID FOR Hydra is:

Devnet: hyDQ4Nz1eYyegS6JfenyKwKzYxRsCWCriYSAjtzP4Vg

Testnet: Do you want this?

Mainnet: hyDQ4Nz1eYyegS6JfenyKwKzYxRsCWCriYSAjtzP4Vg

Basic Flow

A Hydra Wallet's lifecycle has 3 phases:

  1. Creation - Create the Wallet
  2. Member Addition - Add Members and specify their share
  3. Distribution - Distribute funds to the Members according to their share

The Distribution phase is an on-chain operation that's called on a per-Member basis. We'll get into all the details of this later, but Hydra will track all distributions and ensure that Members always get their fair share of the funds. As new funds flow into the Hydra Wallet, members (or other automated processes) will call the Distribution operation to disburse the appropriate share of funds to the given Member.

Let's get into a bit more detail on these steps.

Creating a Wallet

The creator of the Hydra Wallet is known as the Authority. The Authority will specify the globally unique name of the wallet, the total number of shares to be distributed, and the Membership Model (which we'll cover in a moment). We've provided our own FanoutClient with the SDK, which you'll initialize with the Authority's Wallet. You'll need about XXX Sol to create the Hydra Wallet.

const connection = new Connection("devnet", "confirmed");
let fanoutSdk: FanoutClient;

authorityWallet = Keypair.generate();

fanoutSdk = new FanoutClient(
connection,
new NodeWallet(new Account(authorityWallet.secretKey))
);

const init = await fanoutSdk.initializeFanout({
totalShares: 100,
name: `Test${Date.now()}`,
membershipModel: MembershipModel.Wallet,
});

Also accept SPL Tokens

If you want to also accept other specific SPL Tokens, you can update your Hydra Wallet to accept those by specifying the given token's public key after initializing the wallet.

const mintPublicKey = "SPL-Token-Public-Key";

const { fanoutForMint, tokenAccount } = await fanoutSdk.initializeFanoutForMint(
{
fanout,
mint: mint.publicKey,
}
);

Adding Members

There are three different Membership Models shipping with this first version of Hydra:

  1. Wallet - This is the simplest membership model. It's just a list of each Member's public address and the number of shares they own. The sum of all Member's shares must equal the totalShares specified in the initializeFanout call.
const member = new Keypair();

const { membershipAccount } = await fanoutSdk.addMemberWallet({
fanout: init.fanout,
fanoutNativeAccount: init.nativeAccount,
membershipKey: member.publicKey,
shares: 10
});

// Add members until sum of shares = totalShares
...
  1. NFT - With this model membership is tied to an NFT mint address instead of static public address. Each NFT mint address can still have a different quantity of shares as in the Wallet model. The greatest benefit of this model is it effectively enables the simple transfer of rights to future distributions to any wallet owner that holds the given NFT.

const nftMintPublicKey = "nftMintPublicKey";

const init = await fanoutSdk.initializeFanout({
totalShares: 100,
name: `Test${Date.now()}`,
membershipModel: MembershipModel.NFT,
});

const { membershipAccount } = await fanoutSdk.addMemberNft({
fanout: init.fanout,
fanoutNativeAccount: init.nativeAccount,
membershipKey: nftMintPublicKey,
shares: 10
});

// Add members until sum of shares = totalShares
...
  1. Token - This is the most flexible membership model, but is a bit more complicated. In this model, Membership is associated with staked ownership of the specified Token. When creating a Hydra Wallet with the Token model, you specify the mint of an SPL Token and distribute those tokens to your members (in whatever proportion you want). Then those members need to stake their tokens with the Hydra Wallet to be able to claim their share of the distribution.

    For example, if you mint a supply of 1000 tokens and distribute all 1000, but only 40 of them are staked, then distributions will be calculated off of the 40 that are staked, not the 1000 total supply. Members who do not stake get 0% and those that do get staked / 40 of the distribution.

    We are aware of some initialization issues with this model, so for now we recommend you don't fund the Hydra Wallet until you've given your members enough time to stake their tokens.

const membershipMintPublicKey = "SPL-TokenPublicKey";

const { fanout } = await fanoutSdk.initializeFanout({
totalShares: 0,
name: `Test${Date.now()}`,
membershipModel: MembershipModel.Token,
mint: membershipMintPublicKey,
});

// Staking tokens

const ixs = await fanoutSdk.stakeTokenMemberInstructions({
shares: supply * 0.1,
fanout: fanout,
membershipMintTokenAccount: tokenAcctMember,
membershipMint: membershipMint.publicKey,
member: member.publicKey,
payer: member.publicKey,
});

const tx = await fanoutSdk.sendInstructions(
ixs.instructions,
[member],
member.publicKey
);
if (!!tx.RpcResponseAndContext.value.err) {
const txdetails = await connection.getConfirmedTransaction(
tx.TransactionSignature
);
console.log(txdetails, tx.RpcResponseAndContext.value.err);
}

const stake = await membershipMint.getAccountInfo(ixs.output.stakeAccount);
info

NOTE: Some Hydra use cases are Airdropping the membership token to the members. In this case, you may want to stake then tokens on the members' behalf. The most effective way to do this is to use the stakeForTokenMemberInstructions method. In the example below note that the membershipMintTokenAccount is the ATA of the Authority not the member. In this way you are simply sending the membership tokens to the member's stake account not their personal ATA for the membership mint

// Example of setting up a Hydra with a in Memory keypair.
let authorityWallet = Keypair.generate();
let fanoutSdk = new FanoutClient(
connection,
new NodeWallet(new Account(authorityWallet.secretKey))
);
// Setup a Hydra -> Since you configured the SDK with the authority Wallet as the wallet you dont need to pass the signer into the init.
const { fanout } = await fanoutSdk.initializeFanout({
totalShares: 0,
name: `Test${Date.now()}`,
membershipModel: MembershipModel.Token,
mint: membershipMint.publicKey
});

...
const ixs = await fanoutSdk.stakeForTokenMemberInstructions(
{
shares: supply * .1,
fanout: fanout,
membershipMintTokenAccount: tokenAcct,
membershipMint: membershipMint.publicKey,
fanoutAuthority: authorityWallet.publicKey,
member: member.publicKey,
payer: authorityWallet.publicKey
}
);

Distributing Funds

The Distribute method is expected to be called many times over the lifetime of a Hydra Wallet. In order to keep the processing and memory costs under Solana limits while enabling arbitrarily large membership sets, we require that you specify the Member (and if applicable the Member's NFT or SPL mint) that you want to distribute funds to.

caution

Distribution will fail if the sum of member shares does not equal the totalShares specified in the initializeFanout call.

Hydra will track distribution, so you can call this multiple times and funds will only be distributed to the Member once. The Distribute method is slightly different depending on the Membership Model:

Wallet

const member1
.
publicKey = "Member1.publicKey";
const distributionBot = new Keypair();
// This is the caller of the Distribute method, it can be a bot or a user,
// they just need enough funds to pay for the transaction fee. If you're using
// this code, airdrop a sol to distributionBot.

let distributeToMember1 = await fanoutSdk.distributeWalletMemberInstructions(
{
distributeForMint: false,
member: member1.publicKey,
fanout: fanout, // From initialization
payer: distributionBot.publicKey,
},
);

const tx = await fanoutSdk.sendInstructions(
[...distMember1.instructions],
[distributionBot],
distributionBot.publicKey
);
if (!!tx.RpcResponseAndContext.value.err) {
const txdetails = await connection.getConfirmedTransaction(tx.TransactionSignature);
console.log(txdetails, tx.RpcResponseAndContext.value.err);
}

NFT

Same as above, but replace distributeToMember1 with this:

const member1
.
mint = "NFT Mint for Member 1";

let distributeToMember1 = await fanoutSdk.distributeNftMemberInstructions(
{
distributeForMint: false,
member: member1.publicKey,
membershipKey: member1.mint,
fanout: fanout,
payer: distributionBot.publicKey,
},
);

Token

Same as Wallet, but replace distributeToMember1 with this:

const membershiptMint
.
publicKey = "SPL-Token-PublicKey";

let distributeToMember1 = await fanoutSdk.distributeTokenMemberInstructions(
{
distributeForMint: false,
membershipMint: membershipMint.publicKey,
fanout: fanout,
member: member1.publicKey,
payer: distributionBot.publicKey,
}
);

Distribute SPL Tokens

The process is basically the same, you'll additionally specify the Mint of the Token you want to distribute and set distributeForMint to true.

Example for the Wallet member model:

const mint
.
publicKey = "SPL-Token-To-Distribute-PublicKey";

let distributeToMember1 = await fanoutSdk.distributeWalletMemberInstructions(
{
distributeForMint: true,
member: member1.publicKey,
fanout: builtFanout.fanout,
payer: distributionBot.publicKey,
fanoutMint: mint.publicKey
},
);

Additional Capabilities

Signing Metadata as Creator

One key use case for Hydra is specifying the Hydra Wallet as a creator with some royalty share for an NFT. We've enabled the Authority of the Hydra Wallet to sign NFTs as the Hydra Wallet so the wallet is a verified creator in the NFT metadata. We've left out the details of creating the NFT, but assuming you've set the Hydra wallet a creator via init.fanout, you can sign with the instruction below.

// Create Hydra as above.

// Set Royalties
const allCreators = [
{ creator: authorityWallet.publicKey, share: 0 },
{
creator: init.fanout,
publicKey,
share: 100,
},
];

// CREATE NFT Code Adding allCreators as Creator for the NFT

const instructions: TransactionInstruction[] = [];
instructions.push(
/// Create NFT Instructions
/// Sign the nft
...fanoutSdk.signMetadataInstructions({
metadata: metadataAccount,
holdingAccount: init.nativeAccount,
fanout: init.fanout,
}).instructions
);

///....send instructions to solana