Skip to content

Commit

Permalink
Merge pull request #714 from helium/feat/incentive-program-entities
Browse files Browse the repository at this point in the history
Add incentive escrow programs to mobile entity manager
  • Loading branch information
ChewingGlass authored Oct 16, 2024
2 parents f711c67 + 8b5b4b4 commit 4fe1a13
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 3 deletions.
134 changes: 134 additions & 0 deletions packages/helium-admin-cli/src/upsert-incentive-program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as anchor from "@coral-xyz/anchor";
import { subDaoKey } from "@helium/helium-sub-daos-sdk";
import { carrierKey, incentiveProgramKey, init as initMem } from "@helium/mobile-entity-manager-sdk";
import { MOBILE_MINT } from "@helium/spl-utils";
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import Squads from "@sqds/sdk";
import os from "os";
import yargs from "yargs/yargs";
import { sendInstructionsOrSquads } from "./utils";

export async function run(args: any = process.argv) {
const yarg = yargs(args).options({
wallet: {
alias: "k",
describe: "Anchor wallet keypair",
default: `${os.homedir()}/.config/solana/id.json`,
},
url: {
alias: "u",
default: "http://127.0.0.1:8899",
describe: "The solana url",
},
carrierName: {
alias: "c",
type: "string",
required: true,
describe: "The name of the carrier",
},
name: {
alias: "n",
type: "string",
required: true,
describe: "The name of the incentive program",
},
dntMint: {
type: "string",
required: true,
describe: "The subdao mint",
default: MOBILE_MINT.toBase58(),
},
shares: {
type: "number",
required: true,
describe: "The number of rewards shares this program should have",
},
startTs: {
type: "number",
required: true,
describe: "The start timestamp of the incentive program in epoch seconds",
},
stopTs: {
type: "number",
required: true,
describe: "The start timestamp of the incentive program in epoch seconds",
},
metadataUrl: {
type: "string",
required: true,
describe: "The metadata url",
},
recipient: {
type: "string",
required: true,
describe: "The recipient of the incentive program NFT",
},
multisig: {
type: "string",
describe:
"Address of the squads multisig for subdao authority. If not provided, your wallet will be the authority",
},
});

const argv = await yarg.argv;
process.env.ANCHOR_WALLET = argv.wallet;
process.env.ANCHOR_PROVIDER_URL = argv.url;
anchor.setProvider(anchor.AnchorProvider.local(argv.url));
const provider = anchor.getProvider() as anchor.AnchorProvider;
const carrierName = argv.carrierName;
const memProgram = await initMem(provider);
const dntMint = new PublicKey(argv.dntMint);
const subDao = (await subDaoKey(dntMint))[0];

const carrier = await carrierKey(subDao, carrierName)[0];
const carrierAcc = await memProgram.account.carrierV0.fetch(carrier);
const issuingAuthority = carrierAcc.issuingAuthority;
const incentiveProgramK = incentiveProgramKey(carrier, argv.name)[0];
const incentiveProgram = await memProgram.account.incentiveEscrowProgramV0.fetchNullable(incentiveProgramK);

let instructions: TransactionInstruction[] = []
if (!incentiveProgram) {
console.log("Creating incentive program");
instructions.push(
await memProgram.methods
.initializeIncentiveProgramV0({
metadataUrl: argv.metadataUrl,
shares: argv.shares,
startTs: new anchor.BN(argv.startTs),
stopTs: new anchor.BN(argv.stopTs),
name: argv.name,
})
.accounts({ carrier, recipient: new PublicKey(argv.recipient), issuingAuthority })
.instruction()
);
} else {
instructions.push(
await memProgram.methods.updateIncentiveProgramV0({
startTs: new anchor.BN(argv.startTs),
stopTs: new anchor.BN(argv.stopTs),
shares: argv.shares,
})
.accounts({
carrier,
incentiveEscrowProgram: incentiveProgramK,
issuingAuthority
})
.instruction()
);
}

// @ts-ignore
const squads = Squads.endpoint(process.env.ANCHOR_PROVIDER_URL, provider.wallet, {
commitmentOrConfig: "finalized",
});
await sendInstructionsOrSquads({
provider,
instructions,
signers: [],
payer: provider.wallet.publicKey,
commitment: "confirmed",
executeTransaction: false,
multisig: argv.multisig ? new PublicKey(argv.multisig) : undefined,
squads,
});
}
20 changes: 20 additions & 0 deletions packages/mobile-entity-manager-sdk/src/pdas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { PublicKey } from "@solana/web3.js";
import { PROGRAM_ID } from "./constants";
import { sha256 } from "js-sha256";
// @ts-ignore
import bs58 from "bs58";

export const carrierCollectionKey = (
carrer: PublicKey,
Expand All @@ -15,3 +18,20 @@ export const carrierKey = (subDao: PublicKey, name: String, programId: PublicKey
[Buffer.from("carrier", "utf-8"), subDao.toBuffer(), Buffer.from(name, "utf-8")],
programId
);

export const incentiveProgramKey = (
carrier: PublicKey,
name: string,
programId: PublicKey = PROGRAM_ID
) => {
const hash = sha256(name);

return PublicKey.findProgramAddressSync(
[
Buffer.from("incentive_escrow_program", "utf-8"),
carrier.toBuffer(),
Buffer.from(hash, "hex"),
],
programId
);
};
15 changes: 14 additions & 1 deletion packages/mobile-entity-manager-sdk/src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { PROGRAM_ID } from "./constants";
import { PROGRAM_ID as HELIUM_ENTITY_MANAGER_PROGRAM_ID } from "@helium/helium-entity-manager-sdk";
import { init } from "./init";
import { AnchorProvider } from "@coral-xyz/anchor";
import { incentiveProgramKey } from "./pdas";

export const mobileEntityManagerResolvers = combineResolvers(
heliumCommonResolver,
Expand All @@ -25,7 +26,7 @@ export const mobileEntityManagerResolvers = combineResolvers(
return programApprovalKey(accounts.dao as PublicKey, PROGRAM_ID)[0];
}
}),
resolveIndividual(async ({ idlIx, path, accounts, provider }) => {
resolveIndividual(async ({ idlIx, path, accounts, provider, args }) => {
if (
path[path.length - 1] === "keyToAsset" &&
accounts.carrier &&
Expand All @@ -41,6 +42,18 @@ export const mobileEntityManagerResolvers = combineResolvers(
accounts.dao as PublicKey,
Buffer.from(carrier.name, "utf-8")
)[0];
} else if (path[path.length - 1] === "incentiveEscrowProgram" && accounts.carrier && args[0].name) {
return incentiveProgramKey(accounts.carrier as PublicKey, args[0].name)[0];
} else if (
path[path.length - 1] === "keyToAsset" &&
accounts.carrier &&
accounts.dao &&
idlIx.name === "initializeIncentiveProgramV0"
) {
return keyToAssetKey(
accounts.dao as PublicKey,
Buffer.from(args[0].name, "utf-8")
)[0];
}
}),
ataResolver({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use account_compression_cpi::{program::SplAccountCompression, Noop};
use anchor_lang::{prelude::*, solana_program::hash::hash};
use anchor_spl::token::Mint;
use bubblegum_cpi::{program::Bubblegum, TreeConfig};
use helium_entity_manager::{
cpi::{accounts::IssueProgramEntityV0, issue_program_entity_v0},
program::HeliumEntityManager,
IssueProgramEntityArgsV0, KeySerialization, ProgramApprovalV0,
};
use helium_sub_daos::{DaoV0, SubDaoV0};

use crate::{carrier_seeds, error::ErrorCode, state::*};

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default)]
pub struct InitializeIncentiveProgramArgsV0 {
pub name: String,
pub metadata_url: Option<String>,
pub start_ts: i64,
pub stop_ts: i64,
pub shares: u32,
}

#[derive(Accounts)]
#[instruction(args: InitializeIncentiveProgramArgsV0)]
pub struct InitializeIncentiveProgramV0<'info> {
#[account(mut)]
pub payer: Signer<'info>,
pub issuing_authority: Signer<'info>,
#[account(
seeds = ["program_approval".as_bytes(), dao.key().as_ref(), crate::id().as_ref()],
seeds::program = helium_entity_manager_program.key(),
bump = program_approval.bump_seed,
)]
pub program_approval: Box<Account<'info, ProgramApprovalV0>>,
#[account(
has_one = collection,
has_one = merkle_tree,
has_one = issuing_authority,
has_one = sub_dao,
constraint = carrier.approved @ ErrorCode::CarrierNotApproved
)]
pub carrier: Box<Account<'info, CarrierV0>>,
pub collection: Box<Account<'info, Mint>>,
/// CHECK: Handled by cpi
#[account(
mut,
seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref()],
seeds::program = token_metadata_program.key(),
bump,
)]
pub collection_metadata: UncheckedAccount<'info>,
/// CHECK: Handled By cpi account
#[account(
seeds = ["metadata".as_bytes(), token_metadata_program.key().as_ref(), collection.key().as_ref(), "edition".as_bytes()],
seeds::program = token_metadata_program.key(),
bump,
)]
pub collection_master_edition: UncheckedAccount<'info>,
/// CHECK: Checked via cpi
#[account(
seeds = [b"entity_creator", dao.key().as_ref()],
seeds::program = helium_entity_manager_program.key(),
bump,
)]
pub entity_creator: UncheckedAccount<'info>,
pub dao: Box<Account<'info, DaoV0>>,
#[account(
has_one = dao
)]
pub sub_dao: Box<Account<'info, SubDaoV0>>,
#[account(
mut,
seeds = [
"key_to_asset".as_bytes(),
dao.key().as_ref(),
&hash(args.name.as_bytes()).to_bytes()
],
seeds::program = helium_entity_manager_program.key(),
bump
)]
/// CHECK: Checked in cpi
pub key_to_asset: UncheckedAccount<'info>,
#[account(
init,
payer = payer,
seeds = ["incentive_escrow_program".as_bytes(), carrier.key().as_ref(), &hash(args.name.as_bytes()).to_bytes()],
bump,
space = 8 + 60 + std::mem::size_of::<IncentiveEscrowProgramV0>() + args.name.len(),
)]
pub incentive_escrow_program: Box<Account<'info, IncentiveEscrowProgramV0>>,
#[account(
mut,
seeds = [merkle_tree.key().as_ref()],
seeds::program = bubblegum_program.key(),
bump,
)]
pub tree_authority: Box<Account<'info, TreeConfig>>,
/// CHECK: Used in cpi
pub recipient: AccountInfo<'info>,
/// CHECK: Used in cpi
#[account(mut)]
pub merkle_tree: AccountInfo<'info>,
#[account(
seeds = ["collection_cpi".as_bytes()],
seeds::program = bubblegum_program.key(),
bump,
)]
/// CHECK: Used in cpi
pub bubblegum_signer: UncheckedAccount<'info>,

/// CHECK: Verified by constraint
#[account(address = mpl_token_metadata::ID)]
pub token_metadata_program: AccountInfo<'info>,
pub log_wrapper: Program<'info, Noop>,
pub bubblegum_program: Program<'info, Bubblegum>,
pub compression_program: Program<'info, SplAccountCompression>,
pub system_program: Program<'info, System>,
pub helium_entity_manager_program: Program<'info, HeliumEntityManager>,
}

pub fn handler(
ctx: Context<InitializeIncentiveProgramV0>,
args: InitializeIncentiveProgramArgsV0,
) -> Result<()> {
let seeds: &[&[&[u8]]] = &[carrier_seeds!(ctx.accounts.carrier)];

issue_program_entity_v0(
CpiContext::new_with_signer(
ctx.accounts.helium_entity_manager_program.to_account_info(),
IssueProgramEntityV0 {
payer: ctx.accounts.payer.to_account_info(),
program_approver: ctx.accounts.carrier.to_account_info(),
program_approval: ctx.accounts.program_approval.to_account_info(),
collection_authority: ctx.accounts.carrier.to_account_info(),
collection: ctx.accounts.collection.to_account_info(),
collection_metadata: ctx.accounts.collection_metadata.to_account_info(),
collection_master_edition: ctx.accounts.collection_master_edition.to_account_info(),
entity_creator: ctx.accounts.entity_creator.to_account_info(),
dao: ctx.accounts.dao.to_account_info(),
key_to_asset: ctx.accounts.key_to_asset.to_account_info(),
tree_authority: ctx.accounts.tree_authority.to_account_info(),
recipient: ctx.accounts.recipient.to_account_info(),
merkle_tree: ctx.accounts.merkle_tree.to_account_info(),
bubblegum_signer: ctx.accounts.bubblegum_signer.to_account_info(),
token_metadata_program: ctx.accounts.token_metadata_program.to_account_info(),
log_wrapper: ctx.accounts.log_wrapper.to_account_info(),
bubblegum_program: ctx.accounts.bubblegum_program.to_account_info(),
compression_program: ctx.accounts.compression_program.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
},
seeds,
),
IssueProgramEntityArgsV0 {
entity_key: args.name.as_bytes().to_vec(),
name: args.name.clone(),
symbol: String::from("INCENTIVE"),
approver_seeds: seeds[0].iter().map(|s| s.to_vec()).collect(),
key_serialization: KeySerialization::UTF8,
metadata_url: args.metadata_url,
},
)?;

ctx
.accounts
.incentive_escrow_program
.set_inner(IncentiveEscrowProgramV0 {
start_ts: args.start_ts,
stop_ts: args.stop_ts,
shares: args.shares,
carrier: ctx.accounts.carrier.key(),
name: args.name,
bump_seed: ctx.bumps["incentive_escrow_program"],
});

Ok(())
}
4 changes: 4 additions & 0 deletions programs/mobile-entity-manager/src/instructions/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
pub mod approve_carrier_v0;
pub mod initialize_carrier_v0;
pub mod initialize_incentive_program_v0;
pub mod initialize_subscriber_v0;
pub mod issue_carrier_nft_v0;
pub mod revoke_carrier_v0;
pub mod update_carrier_tree_v0;
pub mod update_carrier_v0;
pub mod update_incentive_program_v0;

pub use approve_carrier_v0::*;
pub use initialize_carrier_v0::*;
pub use initialize_incentive_program_v0::*;
pub use initialize_subscriber_v0::*;
pub use issue_carrier_nft_v0::*;
pub use revoke_carrier_v0::*;
pub use update_carrier_tree_v0::*;
pub use update_carrier_v0::*;
pub use update_incentive_program_v0::*;
Loading

0 comments on commit 4fe1a13

Please sign in to comment.