Skip to content

Commit

Permalink
Create script for adding metadata to conditional tokens (#240)
Browse files Browse the repository at this point in the history
* try using helia/ipfs

* Revert "try using helia/ipfs"

This reverts commit 48fd104.

* feat: add shdw

* chore: add instructions

* feat: add jsons

* feat: add shdw provider

* feat: skeleton of metadataattachment

* feat: working script

* feat: move assets

* feat: working script

* refactor

* add data

* refactor: name change

* feat: create fileUploader class

* refactor: finish script

* refactor: delete shdw

* fix prio fees

* Update yarn.lock

* Revert "Update yarn.lock"

This reverts commit fd8e0b9.

* Reapply "Update yarn.lock"

This reverts commit 00184c1.

* change folder names

* update folder names

* feat: add symbol fetching

* delete

* feat: refactor

* reformat
  • Loading branch information
Aiyualive authored Aug 14, 2024
1 parent cbab99d commit 6cce920
Show file tree
Hide file tree
Showing 9 changed files with 780 additions and 487 deletions.
4 changes: 4 additions & 0 deletions scripts/assets/META/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"passImage": "https://arweave.net/iuqi7PRRESdDxj1oRyk2WzR90_zdFcmZsuWicv3XGfs",
"failImage": "https://arweave.net/tGxvOjMZw7B0qHsdCcIMO57oH5g5OaItOZdXo3BXKz8"
}
File renamed without changes
File renamed without changes
4 changes: 4 additions & 0 deletions scripts/assets/USDC/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"passImage": "https://arweave.net/e4IO7F59F_RKCiuB--_ABPot7Qh1yFsGkWzVhcXuKDU",
"failImage": "https://arweave.net/DpvxeAyVbaoivhIVCLjdf566k2SwVn0YVBL0sTOezWk"
}
File renamed without changes
File renamed without changes
135 changes: 109 additions & 26 deletions scripts/attachMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,122 @@
import { AutocratClient, ConditionalVaultClient } from "@metadaoproject/futarchy";
import * as anchor from "@coral-xyz/anchor";
import {
AutocratClient,
ConditionalVaultClient,
} from "@metadaoproject/futarchy";
import { ComputeBudgetProgram, PublicKey } from "@solana/web3.js";
import { DRIFT, USDC } from "./consts";
import { MPL_TOKEN_METADATA_PROGRAM_ID } from "@metadaoproject/futarchy";

const pDRIFT = "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/7f095bfc-9f48-49b6-ca22-a5f2799c9a00/public";
const fDRIFT = "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/cb191bce-1f8b-4faf-fdc1-f80e73b84500/public";

const pFUTURE = "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/235d74f1-3750-4944-d97a-fc6d61955700/public";
const fFUTURE = "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/e69d51c5-fcfa-405e-657e-253185b4d800/public";

const pUSDC = "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/f38677ab-8ec6-4706-6606-7d4e0a3cfc00/public";
const fUSDC = "https://imagedelivery.net/HYEnlujCFMCgj6yA728xIw/d9bfd8de-2937-419a-96f6-8d6a3a76d200/public";

const proposal: PublicKey = new PublicKey("9jAnAupCdPQCFvuAMr5ZkmxDdEKqsneurgvUnx7Az9zS");
import { MetadataHelper } from "./uploadOffchainMetadata";

let autocratClient: AutocratClient = AutocratClient.createClient({
provider: anchor.AnchorProvider.env(),
});

let vaultClient: ConditionalVaultClient = autocratClient.vaultClient;

async function main() {
const storedProposal = await autocratClient.getProposal(proposal);
type TokenMetadata = {
name: string;
image: string;
symbol: string;
description: string;
};

async function main(proposal: string) {
const { quoteVault, baseVault, number } = await autocratClient.getProposal(
new PublicKey(proposal)
);
const { underlyingTokenMint: quotePubkey } = await vaultClient.getVault(
quoteVault
);
const { underlyingTokenMint: basePubkey } = await vaultClient.getVault(
baseVault
);

// 0. Get token symbols
const metadataHelper = new MetadataHelper(autocratClient.provider);
const quoteTokenSymbol = (
await metadataHelper.fetchTokenMetadataSymbol(quotePubkey)
)
.toUpperCase()
.trim();
const baseTokenSymbol = (
await metadataHelper.fetchTokenMetadataSymbol(basePubkey)
)
.toUpperCase()
.trim();

// 1. Read and check image
const quoteTokenImages = await metadataHelper.tryGetTokenImageUrls(
`${__dirname}/assets/`,
quoteTokenSymbol
);
const baseTokenImages = await metadataHelper.tryGetTokenImageUrls(
`${__dirname}/assets/`,
baseTokenSymbol
);
const imageMap = {
[`p${quoteTokenSymbol}`]: quoteTokenImages.passImage,
[`f${quoteTokenSymbol}`]: quoteTokenImages.failImage,
[`p${baseTokenSymbol}`]: baseTokenImages.passImage,
[`f${baseTokenSymbol}`]: baseTokenImages.failImage,
};

if (!Object.values(imageMap).every((x) => x)) {
throw Error("Image files do not exist");
}

// 2. Create metadata URI and upload it
// Should probably try read locally and if not exists then upload and save it
const uris = await Promise.all(
Object.keys(imageMap).map((cToken) => {
const market = cToken.toLowerCase().startsWith("p") ? "pass" : "fail";

const conditionalTokenMetadata: TokenMetadata = {
name: `Proposal ${number}: ${cToken}`,
image: imageMap[cToken],
symbol: cToken,
description: `Native token in the MetaDAO's conditional ${market} market for proposal ${number}`,
};

return metadataHelper.uploadImageJson(conditionalTokenMetadata);
})
);

if (uris.some((uri) => !uri)) {
throw new Error(
"An error occurred while uploading one or more JSON metadata files"
);
}

const [pQuoteUri, fQuoteUri, pBaseUri, fBaseUri] = uris;

const { baseVault, quoteVault } = storedProposal;
console.log(baseVault);
try {
const txId = await vaultClient
.addMetadataToConditionalTokensIx(
quoteVault,
new PublicKey(quotePubkey),
number,
pQuoteUri,
fQuoteUri
)
.preInstructions([
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
ComputeBudgetProgram.setComputeUnitLimit({ units: 250_000 }),
await vaultClient
.addMetadataToConditionalTokensIx(
baseVault,
new PublicKey(basePubkey),
number,
pBaseUri,
fBaseUri
)
.instruction(),
])
.rpc({ skipPreflight: true });

vaultClient.addMetadataToConditionalTokensIx(quoteVault, USDC, 1, pUSDC, fUSDC)
.preInstructions([
await vaultClient.addMetadataToConditionalTokensIx(baseVault, DRIFT, 1, pDRIFT, fDRIFT).instruction(),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 }),
ComputeBudgetProgram.setComputeUnitLimit({ units: 250_000 }),
])
.rpc()
console.log("Signature", txId);
} catch (e) {
console.log("error", e);
}
}

main();
// Usage: anchor run attach -- <proposal>
main(process.argv[2]);
95 changes: 91 additions & 4 deletions scripts/uploadOffchainMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@ import * as anchor from "@coral-xyz/anchor";
import {
Metadata,
deserializeMetadata,
fetchDigitalAsset,
findMetadataPda,
} from "@metaplex-foundation/mpl-token-metadata";
import {
GenericFile,
Umi,
createGenericFile,
keypairIdentity,
publicKey,
signerIdentity,
} from "@metaplex-foundation/umi";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { bundlrUploader } from "@metaplex-foundation/umi-uploader-bundlr";
import {
fromWeb3JsPublicKey,
toWeb3JsPublicKey,
} from "@metaplex-foundation/umi-web3js-adapters";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import { bundlrUploader } from "@metaplex-foundation/umi-uploader-bundlr";
import { PublicKey } from "@solana/web3.js";

import fs from "fs";
import path from "path";

const uploadedAssetMap: Record<string, string> = {
fMETA: "https://arweave.net/tGxvOjMZw7B0qHsdCcIMO57oH5g5OaItOZdXo3BXKz8",
Expand Down Expand Up @@ -240,4 +241,90 @@ export const fetchOnchainMetadataForMint = async (
};
};

type ImageData = {
failImage: string;
passImage: string;
};

export class MetadataHelper {
public umi: Umi;

constructor(private provider: anchor.AnchorProvider) {
const payer = this.provider.wallet["payer"];
this.umi = createUmi(this.provider.connection);
this.umi.use(keypairIdentity(payer));
this.umi.use(bundlrUploader());
}

async uploadImageJson(json: any) {
return this.umi.uploader.uploadJson(json, {
onProgress: (percent: number, ...args: any) => {
console.log(`percent metadata upload progress ${percent}`);
console.log("progress args: ", args);
},
});
}

async uploadImageFromFile(filename: string, filepath: string) {
const data = fs.readFileSync(path.join(filepath, filename));
const file = createGenericFile(new Uint8Array(data), filename, {
contentType: "image/png",
});
return await this.umi.uploader.upload([file]);
}

async uploadImagesFromFolder(folderName: string) {
const folder = fs.readdirSync(`${__dirname}/assets/${folderName}`);

const filesToUpload: Array<GenericFile> = [];
for (const filename of folder) {
if (!filename.endsWith(".png")) continue;

const data = fs.readFileSync(`${__dirname}/assets/${filename}`);
filesToUpload.push(
createGenericFile(new Uint8Array(data), filename, {
contentType: "image/png",
})
);
}

const uris = await this.umi.uploader.upload(filesToUpload);

return uris.map((uri, idx) => {
return {
uri,
name: filesToUpload[idx].fileName.split(".")[0],
};
});
}

async fetchTokenMetadataSymbol(pubkey: PublicKey) {
const { metadata } = await fetchDigitalAsset(
this.umi,
fromWeb3JsPublicKey(pubkey)
);
return metadata.symbol;
}

async tryGetTokenImageUrls(basePath: string, token: string) {
const filepath = path.join(basePath, token);
const imageData = path.join(filepath, "data.json");

let images: ImageData;
try {
images = JSON.parse(fs.readFileSync(imageData, "utf8")) as ImageData;
} catch (e) {
images = {
failImage: await this.uploadImageFromFile(`f${token}.png`, basePath)[0],
passImage: await this.uploadImageFromFile(`p${token}.png`, basePath)[0],
};

// Save the file
fs.writeFileSync(imageData, JSON.stringify(images, null, "\t"));
}

return images;
}
}

// fetchOnchainMetadataForMint();
Loading

0 comments on commit 6cce920

Please sign in to comment.