Link an Account to Image, Text and Time Metadata with On-Chain Storage
Create Rust LookupMap data structures that store image IPFS URIs, text content and timestamps linked to NEAR wallet addresses. Learn how to put & fetch these efficiently via the UI.
Follow the Project Setup instructions to start the project in your local machine and try the recipes yourself.
You can also clone or navigate the repo to get just what you need.
In the previous chapter:
we explain how to create new game contracts and how some of its storage is set upon initialization:
This technique has been in the blockchain industry for a while now, instead of storing the entire image file object, store only an IPFS hash of the image, or its equivalent.
Prompt Wars has IPFS utilities that you may implement in your dApp to achieve similar results:
These IPFS helpers are then used to orchestrate a new game contract:
Storing player data
Once a game is created, a player may submit a text prompt. Follow this walkthrough to learn how to link data to a player, on chain.
When a player submits a prompt, we call ft_transfer_call on the corresponding NEP141 contract, in this case USDT . If the transfer succeeds, it will make a cross-contract call to the game contract:
Let's look at the payload structure:
#[derive(Serialize, Deserialize)]pubstructCreateOutcomeTokenArgs {// the outcome value, in this case, the prompt submitted to the competitionpub prompt:String,}#[derive(Serialize, Deserialize)]pubenumPayload {CreateOutcomeTokenArgs(CreateOutcomeTokenArgs),}
Setting values from the client side
Once a contract client interface is instantiated, we may call its CHANGE methods:
The clue to this storage strategy is in the definition of
#[derive(BorshSerialize, BorshDeserialize, Serialize)]pubstructOutcomeToken {// the account id of the outcome_tokenpub outcome_id:OutcomeId,// the outcome value, in this case, the prompt submitted to the competitionpub prompt:String,// the outcome value, in this case, the prompt submitted to the competitionpub output_img_uri:Option<String>,// store the result from the image comparison: percentage_diff or pixel_differencepub result:Option<OutcomeTokenResult>,// total supply of this outcome_tokenpub total_supply:WrappedBalance,}
an OutcomeToken is our naming convention of a player's prompt data, the image URI that got rendered from its prompt and the collateral token balance relative to the player's NEP141 deposit:
Rendering images from a player's OutcomeToken
The OutcomeToken implementation has VIEW methods useful for getting any player's prompt data:
import { v4 as uuidv4 } from "uuid";
import { NextApiRequest, NextApiResponse } from "next";
import logger from "providers/logger";
import { DeployPromptWarsMarketContractArgs } from "providers/near/contracts/prompt-wars-market-factory/prompt-wars-market-factory.types";
import pulse from "providers/pulse";
import { CollateralToken, Management, MarketData } from "providers/near/contracts/prompt-wars/prompt-wars.types";
import near from "providers/near";
import { PromptWarsMarketFactory } from "providers/near/contracts/prompt-wars-market-factory/contract";
import { PromptWarsMarketContract } from "providers/near/contracts/prompt-wars/contract";
import ipfs from "providers/ipfs";
import { routes } from "hooks/useRoutes/useRoutes";
export default async function Fn(_request: NextApiRequest, response: NextApiResponse) {
try {
const latestMarketId = await pulse.promptWars.getLatestMarketId();
if (await pulse.promptWars.isMarketActive(latestMarketId!)) {
throw new Error("ERR_LATEST_MARKET_IS_STILL_ACTIVE");
}
const image_uri = await ipfs.getFileAsIPFSUrl("https://source.unsplash.com/random/512x512");
const market: MarketData = {
image_uri,
// Set to 0, it will be set in the contract initialization
starts_at: 0,
// Set to 0, it will be set in the contract initialization
ends_at: 0,
};
const management: Management = {
dao_account_id: near.getConfig().marketDaoAccountId,
market_creator_account_id: near.getConfig().serverWalletId,
// Set to 0, it will be set in the contract initialization
self_destruct_window: 0,
// Set to 0, it will be set in the contract initialization
buy_sell_threshold: 0,
};
const { accountId, decimals } = pulse.getDefaultCollateralToken();
const collateral_token: CollateralToken = {
id: accountId,
decimals,
balance: 0,
fee_balance: 0,
};
const promptWarsMarketArgs: DeployPromptWarsMarketContractArgs = {
market,
management,
collateral_token,
};
const id = `pw-${uuidv4().slice(0, 4)}`;
await PromptWarsMarketFactory.createMarket(id, promptWarsMarketArgs);
logger.info({ promptWarsMarketArgs, id });
const marketId = `${id}.${near.getConfig().factoryWalletId}`;
const marketContract = await PromptWarsMarketContract.loadFromGuestConnection(marketId);
const marketData = await marketContract.get_market_data();
const resolution = await marketContract.get_resolution_data();
let ms = marketData.ends_at - marketData.starts_at;
const revealEndpoint = routes.api.promptWars.reveal();
logger.info(`setting timeout to call the reveal API endpoint ${revealEndpoint} for market ${marketId} in ${ms} ms`);
setTimeout(async () => {
try {
logger.info(`calling the reveal API endpoint ${revealEndpoint} for market ${marketId}`);
await fetch(revealEndpoint);
} catch (error) {
logger.error(error);
}
}, ms);
ms = resolution.reveal_window - marketData.starts_at;
const resolveEndpoint = routes.api.promptWars.resolve();
logger.info(
`setting timeout to call the resolution API endpoint ${resolveEndpoint} for market ${marketId} in ${ms} ms`,
);
setTimeout(async () => {
try {
logger.info(`calling resolution API endpoint ${resolveEndpoint} for market ${marketId}`);
await fetch(resolveEndpoint);
} catch (error) {
logger.error(error);
}
}, ms);
response.status(200).json({ promptWarsMarketArgs, id });
} catch (error) {
logger.error(error);
response.status(500).json({ error: (error as Error).message });
}
}