Metaplex Token Authorization Rules
A program that provides the ability to create and execute rules to restrict common token operations such as transferring and selling.
Overview
Authorization rules are variants of a Rule
enum that implements a validate()
function.
There are Primitive Rules and Composed Rules that are created by combining of one or more primitive rules.
Primitive Rules store any accounts or data needed for evaluation, and at runtime will produce a true or false output based on accounts and a well-defined Payload
that are passed into the validate()
function.
Composed Rules return a true or false based on whether any or all of the primitive rules return true. Composed rules can then be combined into higher-level composed rules that implement more complex boolean logic. Because of the recursive definition of the Rule
enum, calling validate()
on a top-level composed rule will start at the top and validate at every level, down to the component primitive rules.
Environment Setup
- Install Rust from https://rustup.rs/
- Install Solana from https://docs.solana.com/cli/install-solana-cli-tools#use-solanas-install-tool
- Run
yarn install
to install dependencies
Build the program, generate the JS API, and rebuild IDL (using Shank and Solita)
$ yarn build:rust
$ yarn solita
Build the JS SDK only (must be generated first)
$ yarn build:sdk
Build the program and generate/build the IDL/SDK/docs
$ yarn build
Start Amman and run the test script
Run the following command in a separate shell
$ amman start
Then, run the Amman script
$ yarn amman
Build and test the Rust program
$ cd program/
$ cargo build-bpf
$ cargo test-bpf
CLI
The folder cli
contains a typescript CLI to manage rule set revisions:
Usage: auth [options] [command]
CLI for managing RuleSet revisions.
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
create [options] creates a new rule set revision
convert [options] converts a rule set revision from V1 to V2
print [options] prints the latest rule set revision as a JSON object
help [command] display help for command
To start the CLI, navigate to the folder ./cli
and run
$ yarn install
Then, the CLI can be used as
$ yarn start <COMMAND>
The folder ./examples
contain several examples of rule sets. You will need to replace the owner
pubkey value with the pubkey used to run the CLI.
Note that you need to first build the JS SDK.
Examples
Rust
Note: Additional Rust examples can be found in the program/tests directory.
use mpl_token_auth_rules::{
instruction::{
builders::{CreateOrUpdateBuilder, ValidateBuilder},
CreateOrUpdateArgs, InstructionBuilder, ValidateArgs,
},
payload::{Payload, PayloadType},
state::{CompareOp, Rule, RuleSetV1},
};
use num_derive::ToPrimitive;
use rmp_serde::Serializer;
use serde::Serialize;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
instruction::AccountMeta, native_token::LAMPORTS_PER_SOL, signature::Signer,
signer::keypair::Keypair, transaction::Transaction,
};
#[repr(C)]
#[derive(ToPrimitive)]
pub enum Operation {
OwnerTransfer,
Delegate,
SaleTransfer,
}
impl ToString for Operation {
fn to_string(&self) -> String {
match self {
Operation::OwnerTransfer => "OwnerTransfer".to_string(),
Operation::Delegate => "Delegate".to_string(),
Operation::SaleTransfer => "SaleTransfer".to_string(),
}
}
}
fn main() {
let url = "https://api.devnet.solana.com".to_string();
let rpc_client = RpcClient::new(url);
let payer = Keypair::new();
let signature = rpc_client
.request_airdrop(&payer.pubkey(), LAMPORTS_PER_SOL)
.unwrap();
loop {
let confirmed = rpc_client.confirm_transaction(&signature).unwrap();
if confirmed {
break;
}
}
let (rule_set_addr, _ruleset_bump) = mpl_token_auth_rules::pda::find_rule_set_address(
payer.pubkey(),
"test rule_set".to_string(),
);
let adtl_signer = Keypair::new();
let adtl_signer_rule = Rule::AdditionalSigner {
account: adtl_signer.pubkey(),
};
let amount_rule = Rule::Amount {
amount: 1,
operator: CompareOp::LtEq,
field: "Amount".to_string(),
};
let overall_rule = Rule::All {
rules: vec![adtl_signer_rule, amount_rule],
};
let mut rule_set = RuleSetV1::new("test rule_set".to_string(), payer.pubkey());
rule_set
.add(Operation::OwnerTransfer.to_string(), overall_rule)
.unwrap();
println!("{:#?}", rule_set);
let mut serialized_rule_set = Vec::new();
rule_set
.serialize(&mut Serializer::new(&mut serialized_rule_set))
.unwrap();
let create_ix = CreateOrUpdateBuilder::new()
.payer(payer.pubkey())
.rule_set_pda(rule_set_addr)
.build(CreateOrUpdateArgs::V1 {
serialized_rule_set,
})
.unwrap()
.instruction();
let latest_blockhash = rpc_client.get_latest_blockhash().unwrap();
let create_tx = Transaction::new_signed_with_payer(
&[create_ix],
Some(&payer.pubkey()),
&[&payer],
latest_blockhash,
);
let signature = rpc_client.send_and_confirm_transaction(&create_tx).unwrap();
println!("Create tx signature: {}", signature);
let mint = Keypair::new().pubkey();
let payload = Payload::from([("Amount".to_string(), PayloadType::Number(1))]);
let validate_ix = ValidateBuilder::new()
.rule_set_pda(rule_set_addr)
.mint(mint)
.additional_rule_accounts(vec![AccountMeta::new_readonly(adtl_signer.pubkey(), true)])
.build(ValidateArgs::V1 {
operation: Operation::OwnerTransfer.to_string(),
payload,
update_rule_state: false,
rule_set_revision: None,
})
.unwrap()
.instruction();
let latest_blockhash = rpc_client.get_latest_blockhash().unwrap();
let validate_tx = Transaction::new_signed_with_payer(
&[validate_ix],
Some(&payer.pubkey()),
&[&payer, &adtl_signer],
latest_blockhash,
);
let signature = rpc_client
.send_and_confirm_transaction(&validate_tx)
.unwrap();
println!("Validate tx signature: {}", signature);
}
JavaScript
Note: Additional JS examples can be found in the /cli/ source along with the example rulesets in /cli/examples/
import { createCreateInstruction, createTokenAuthorizationRules, PREFIX, PROGRAM_ID } from './helpers/mpl-token-auth-rules';
import { Keypair, Connection, PublicKey, Transaction, SystemProgram } from '@solana/web3.js';
import {
findRuleSetPDA,
getRuleSetRevisionFromJson,
serializeRuleSetRevision,
} from '@metaplex-foundation/mpl-token-auth-rules';
export const findRuleSetPDA = async (payer: PublicKey, name: string) => {
return await PublicKey.findProgramAddress(
[
Buffer.from(PREFIX),
payer.toBuffer(),
Buffer.from(name),
],
PROGRAM_ID,
);
}
export const createTokenAuthorizationRules = async (
connection: Connection,
payer: Keypair,
name: string,
data: Uint8Array,
) => {
const ruleSetAddress = await findRuleSetPDA(payer.publicKey, name);
let createIX = createCreateOrUpdateInstruction(
{
payer: payer.publicKey,
ruleSetPda: ruleSetAddress[0],
systemProgram: SystemProgram.programId,
},
{
createOrUpdateArgs: {__kind: "V1", serializedRuleSet: data },
},
PROGRAM_ID,
)
const tx = new Transaction().add(createIX);
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;
tx.feePayer = payer.publicKey;
const sig = await connection.sendTransaction(tx, [payer]);
await connection.confirmTransaction(sig, "finalized");
return ruleSetAddress[0];
}
const connection = new Connection("<YOUR_RPC_HERE>", "finalized");
let payer = Keypair.generate()
const revision = getRuleSetRevisionFromJson(JSON.parse(fs.readFileSync("./examples/v2/pubkey-list-match.json")));
await createTokenAuthorizationRules(connection, payer, name, serializeRuleSetRevision(revision));