Table of Contents
Keypom sheds light on the endless power that NEAR's access keys introduce. The contract was created as a result of 3 common problems that arose in the ecosystem.
The contract was initially created as a way to handle the 1 $NEAR minimum deposit required for creating linkdrops using the regular linkdrop contract. If users wanted to create linkdrops, they needed to attach a minimum of 1 $NEAR. This made it costly and unscalable for projects that wanted to mass onboard onto NEAR. Keypom, on the other hand, has been highly optimized and the design can be broken down into two categories: keys, and drops. At the end of the day, Keypom is a utility used to generate keys that belong to a drop. The drop outlines a suite of different permissions and outcomes that each key will derive. At a high level, Keypom allows for 4 different types of drops, each equipped with their own set of customizable features.
Keypom provides a highly customizable set of features for keys to inherit from when drops are created. These features come in the form of an optional The drop config outlines global configurations that all the keys in the drop will inherit from. These configurations are outlined below.
In addition to the drop config, the drop metadata is a way to pass additional information about the drop in the form of an arbitrary string. It's up to the drop owner to decide how this information should be used. A common approach is to pass in stringified JSON outlining a title, description, and media for the drop such that it can be rendered nicely on frontends. When creating either an NFT or FT drop, the creator has the ability to specify 2 different fields:
FT Specific:
NFT Specific:
Keypom allows for suite of features when creating function call drops. This allows for almost endless possibilities for creators. At the top level, each drop will have an optional
In addition to the FCConfig, the creator can specify what's known as
This method data is outlined in the form of a set of optional Let's look at an example of how powerful this can be. Let's say you're doing an NFT ticketing event and want to have a proof of attendance where users will have an NFT lazy minted to them if they actually show up to the event. You could have a key with 2 claims where the first method data is null and the second is a vector of size 1 that will lazy mint an NFT. You could setup an app that claims the null case when the person visits the link you gave them. The bouncer could then give them a password that would allow them to claim the second use and get the NFT. They can only do this if they show up to the event and get the password from the bouncer as the link you gave them is encrypted. As the creator, you would know how many people didn't use your original link, used it but didn't show up, and showed up all by checking the uses of the key. There are several costs that must be taken into account when using Keypom. These costs are broken down into two categories: per key and per drop. On top of these costs, Keypom takes 1 $NEAR per drop and 0.005 $NEAR per key. This model promotes drops with a lot of keys rather than many different drops with fewer keys. These numbers can be changed on a per-account basis so reach out to Ben or Matt if this is of interest to your application.
When creating an empty drop, there are only two costs to keep in mind regardless of the drop type:
Whenever keys are added to a drop (either when the drop is first created or at a later date), the costs are outlined below.
Since keys aren't registered for use until after the contract has received the NFT, we don't know how much storage the token IDs will use on the contract. To combat this, the drop creators must pass in the longest token ID and the contract will charge that storage cost for all key uses. Since accounts claiming FTs may or may not be registered on the Fungible Token contract, Keypom will automatically try to register all accounts. This means that the drop creators must front the cost of registering users depending on the Drop creators have a ton of customization available to them when creation Function Call drops. A cost that they might incur is the attached deposit being sent alongside the function call. Keypom will charge creators for all the attached deposits they specify.
Creators have the ability to delete drops and keys at any time. In this case, all the initial costs they incurred for the remaining keys will be refunded to them except for Keypom's fees. One way that Keypom optimizes the fee structure is by performing automatic refunds for some of the initial costs that creators pay for when keys are used. All the storage that is freed along with any unused allowance is automatically sent back to the creator whenever a key is used. This model drastically reduces the overall costs of creating drops and creates incentives for the keys to be used. In order to make the UX of using Keypom seamless, the contract introduces a debit account model. All costs and refunds go through your account's balance which is stored on the contract. This balance can be topped up or withdrawn at any moment using the |
For some background as to how linkdrops works on NEAR:
The funder that has an account and some $NEAR:
- creates a keypair locally
(pubKey1, privKey1)
. The blockchain doesn't know of this key's existence yet since it's all local for now. - calls
send
on the contract and passes in thepubKey1
as an argument as well as the desiredbalance
for the linkdrop.- The contract will map the
pubKey1
to the desiredbalance
for the linkdrop. - The contract will then add the
pubKey1
as a function call access key with the ability to callclaim
andcreate_account_and_claim
. This means that anyone with theprivKey1
that was created locally, can claim this linkdrop.
- The contract will map the
- Funder will then create a link to send to someone that contains this
privKey1
. The link follows the following format:
wallet.testnet.near.org/linkdrop/{fundingContractAccountId}/{linkdropKeyPairSecretKey}?redirectUrl={redirectUrl}
fundingContractAccountId
: The contract accountId that was used to send the funds.linkdropKeyPairSecretKey
: The corresponding secret key to the public key sent to the contract.redirectUrl
: The url that wallet will redirect to after funds are successfully claimed to an existing account. The URL is sent the accountId used to claim the funds as a query param.
The receiver of the link that is claiming the linkdrop:
- Receives the link which includes
privKey1
and sends them to the NEAR wallet. - Wallet creates a new keypair
(pubKey2, privKey2)
locally. The blockchain doesn't know of this key's existence yet since it's all local for now. - Receiver will then choose an account ID such as
new_account.near
. - Wallet will then use the
privKey1
which has access to callclaim
andcreate_account_and_claim
in order to callcreate_account_and_claim
on the contract.- It will pass in
pubKey2
which will be used to create a full access key for the new account.
- It will pass in
- The contract will create the new account and transfer the funds to it alongside any NFT or fungible tokens pre-loaded.
With Keypom contract, users can pre-load each key with a set of NFTs depending on how many uses per key. Each use will pop the last token ID off and send it to the claimed account.
In order to pre-load NFTs and register a key use, you must:
- Add enough $NEAR to your account balance
- create a drop and specify NFTData for the NFTs that will be sent to the contract.
- Add a key to the drop
- Send an NFT with a token ID shorter than the longest token ID specified
An example of creating an NFT drop can be seen:
near call keypom.testnet create_drop
'{
"public_keys": [
"ed25519:4Adq6WiKVjGz56Ena6D1w2UnADuZpiFBWAz12cfnkibv"
],
"deposit_per_use": "5000000000000000000000",
"nft_data": {
"contract_id": "nft.examples.testnet",
"sender_id": "benjiman.testnet",
"longest_token_id": "ed25519:4Adq6WiKVjGz56Ena6D1w2UnADuZpiFBWAz12cfnkibv"
},
"config": {
"uses_per_key": 2,
},
"metadata": "{\"title\":\"This is a title\",\"description\":\"This is a description\"}"
}'
--accountId "benjiman.testnet"
- Once the regular drop has been created with at least 1 key, execute the
nft_transfer_call
function on the NFT contract and you must pass in the drop ID into themsg
parameter. The token ID will then be pushed to the end of the list of registered token IDs for the drop.
near call nft.examples.testnet nft_transfer_call '{"token_id": "token1", "receiver_id": "keypom.testnet", "msg": "1"}' --accountId "benjiman.testnet" --depositYocto 1
NOTE: you must send the NFT after the drop has been created with at least 1 key. If you send more NFTs than the number of uses left, the NFT will be kept by the contract.
Once the NFT is sent to the contract, it will be registered and you can view the current information about any key using the get_key_information
function. Upon claiming, the NFT will be transferred from the contract to the newly created account (or existing account) along with the balance of the key. If any part of the key claiming process is unsuccessful, both the NFT and the $NEAR will be refunded to the funder and token sender respectively.
NOTE: If the NFT fails to transfer from the contract back to the token sender due to a refund for any reason, the NFT will remain on the Keypom.
With Keypom, users can pre-load a drop with only one type of fungible token due to GAS constraints. The number of fungible tokens, however, is not limited. You could load 1 TEAM token, or a million TEAM tokens. You cannot, however, load 10 TEAM tokens and 50 MIKE tokens at the same time.
Due to the nature of how fungible token contracts handle storage, the user is responsible for attaching enough $NEAR to cover the registration fee. As mentioned in the About section, this amount is dynamically calculated before the drop is created in the create_drop
function. The process for creating fungible token drop is very similar to NFTs:
- Add enough $NEAR to your account balance
- create a drop and specify FTData for the Fungible Tokens that will be sent to the contract.
- Add a key to the drop
- Send the FTs and pass in the drop ID
An example of creating an FT drop can be seen:
export LINKDROP_PROXY_CONTRACT_ID="INSERT_HERE"
export FUNDING_ACCOUNT_ID="INSERT_HERE"
export LINKDROP_NEAR_AMOUNT="INSERT_HERE"
export SEND_MULTIPLE="false"
Once the drop is created with the fungible token data, you can the send the fungible tokens to register uses.
- execute the
ft_transfer_call
function on the FT contract and you must pass in the drop ID into themsg
parameter. An example of this can be:
near call FT_CONTRACT.testnet ft_transfer_call
`{
"receiver_id": "keypom.testnet",
"amount": "25",
"msg": "0"
}`
--accountId "benjiman.testnet" --depositYocto 1
NOTE: you must send the FTs after the drop has been created with at least 1 key. If you send more FTs than the number of uses left, the FTs will be kept by the contract. You are also responsible for registering the Keypom contract for the given fungible token contract if it isn't registered already.
Once the fungible tokens are sent to the contract, they will be registered and you can view the current information about any key using the get_key_information
function. Upon claiming, the contract will register the newly created account (or existing account) on the fungible token contract using the storage you deposited. After this is complete, the fungible tokens will be transferred from the contract to the claimed account along with the balance of the key. If any part of the key claiming process is unsuccessful, both the fungible tokens and the $NEAR will be refunded to the funder and token sender respectively.
NOTE: If the FT fails to transfer from the contract back to the token sender due to a refund for any reason, the fungible tokens will remain on the proxy contract.
Let's look at an example to see the power of the keypom contract. If a user wants to be able to lazy mint two NFTs everytime a key is used but the mint function takes a parameter receiver_id
and a deposit of 1 $NEAR, you could specify these parameters.
If there was a different NFT contract where the parameter was nft_contract_id
instead, that is also possible. You can specify the exact field that the claiming account ID should be passed into. An example flow of creating a function call drop is below.
- create a drop and specify the FC (function call) data for the function that will be called upon claim
near call keypom.testnet send
'{
"public_keys": [
"ed25519:3ANjBcTh6ZNTBqj9KLdTxXtW7ChnuSfc6n4rJMzkXrE9",
],
"deposit_per_use": "5000000000000000000000",
"fc_data": {
"methods": [
[
{
"receiver_id": "nft.examples.testnet",
"method_name": "nft_mint",
"args": "{\"token_id\":\"test-one\",\"metadata\":{\"title\":\"Linkdropped Go Team NFT\",\"description\":\"Testing Linkdrop NFT Go Team Token\",\"media\":\"https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif\",\"media_hash\":null,\"copies\":10000,\"issued_at\":null,\"expires_at\":null,\"starts_at\":null,\"updated_at\":null,\"extra\":null,\"reference\":null,\"reference_hash\":null}}",
"attached_deposit": "1000000000000000000000000"
},
{
"receiver_id": "nft.examples.testnet",
"method_name": "nft_mint",
"args": "{\"token_id\":\"test-two\",\"metadata\":{\"title\":\"Linkdropped Go Team NFT\",\"description\":\"Testing Linkdrop NFT Go Team Token\",\"media\":\"https://bafybeiftczwrtyr3k7a2k4vutd3amkwsmaqyhrdzlhvpt33dyjivufqusq.ipfs.dweb.link/goteam-gif.gif\",\"media_hash\":null,\"copies\":10000,\"issued_at\":null,\"expires_at\":null,\"starts_at\":null,\"updated_at\":null,\"extra\":null,\"reference\":null,\"reference_hash\":null}}",
"attached_deposit": "1000000000000000000000000"
}
]
],
"config": {
"account_id_field": "receiver_id",
"drop_id_field": "custom_drop_id",
"key_id_field": "custom_key_id"
}
},
"config": {
"uses_per_key": 2,
},
"metadata": "{\"title\":\"This is a title\",\"description\":\"This is a description\"}"
}'
--accountId "benjiman.testnet"
This will create a drop with 1 key that can be used 2 times. Everytime the key is used, it will call the nft_mint
function on the NFT contract. The first time it will mint a token with the token ID test-one
and the second time it will mint a token with the token ID test-two
. In addition, the account Id, drop Id, and key Id fields will be sent in the arguments.
Keypom allows users to query a suite of different information from the contract. This information can be broken down into two separate objects that are returned. JsonDrops and JsonKeys.
pub struct JsonDrop {
// Drop ID for this drop
pub drop_id: DropId,
// owner of this specific drop
pub owner_id: AccountId,
// Balance for all keys of this drop. Can be 0 if specified.
pub deposit_per_use: U128,
// Every drop must have a type
pub drop_type: JsonDropType,
// The drop as a whole can have a config as well
pub config: Option<DropConfig>,
// Metadata for the drop
pub metadata: Option<DropMetadata>,
// How many claims
pub registered_uses: u64,
// Ensure this drop can only be used when the function has the required gas to attach
pub required_gas: Gas,
// Keep track of the next nonce to give out to a key
pub next_key_id: u64,
}
pub struct JsonKeyInfo {
// Drop ID for the specific drop
pub drop_id: DropId,
pub pk: PublicKey,
pub key_info: KeyInfo {
// How many uses this key has left. Once 0 is reached, the key is deleted
pub remaining_uses: u64,
// When was the last time the key was used
pub last_used: u64,
// How much allowance does the key have left. When the key is deleted, this is refunded to the funder's balance.
pub allowance: u128,
// Nonce for the current key.
pub key_id: u64,
},
}
get_key_balance(key: PublicKey)
: Returns the $NEAR that will be sent to the claiming account when the key is usedget_key_total_supply()
: Returns the total number of keys currently on the contractget_keys(from_index: Option<U128>, limit: Option<u64>)
: Paginate through all keys on the contract and return a vector of key infoget_key_information(key: PublicKey)
: Return the key info for a specific key
get_drop_information(drop_id: Option<DropId>, key: Option<PublicKey>)
: Return the drop info for a specific drop. This can be queried for by either passing in the drop ID or a public key.get_key_supply_for_drop(drop_id: DropId)
: Return the total number of keys for a specific dropget_keys_for_drop(drop_id: DropId, from_index: Option<U128>, limit: Option<u64>)
: Paginate through all keys for a specific drop and return a vector of key infoget_drop_supply_for_owner(account_id: AccountId)
: Return the total number of drops for a specific accountget_drops_for_owner(account_id: AccountId, from_index: Option<U128>, limit: Option<u64>)
: Paginate through all drops for a specific account and return a vector of drop infoget_nft_supply_for_drop(drop_id: DropId)
: Get the total number of NFTs registered for a given drop.get_nft_token_ids_for_drop(drop_id: DropId, from_index: Option<U128>, limit: Option<u64>)
: Paginate through token IDs for a given dropget_next_drop_id()
: Get the next drop ID that will be used for a new drop
get_root_account()
: Get the global root account that all created accounts with be based off.get_user_balance()
: Get the current user balance for a specific account.
First off, thanks for taking the time to contribute! Contributions are what makes the open-source community such an amazing place to learn, inspire, and create. Any contributions you make will benefit everybody else and are greatly appreciated.
Please try to create bug reports that are:
- Reproducible. Include steps to reproduce the problem.
- Specific. Include as much detail as possible: which version, what environment, etc.
- Unique. Do not duplicate existing opened issues.
- Scoped to a Single Bug. One bug per report.
Please adhere to this project's code of conduct.
You can use markdownlint-cli to check for common markdown style inconsistency.
This project is licensed under the GPL License.
Thanks for these awesome resources that were used during the development of the Keypom Contract: