Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: plugin-story #1030

Merged
merged 22 commits into from
Dec 14, 2024
Merged
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -216,4 +216,5 @@ APTOS_PRIVATE_KEY= # Aptos private key
APTOS_NETWORK= # must be one of mainnet, testnet

# Story
STORY_PRIVATE_KEY= # Story private key
STORY_PRIVATE_KEY= # Story private key
PINATA_JWT= # Pinata JWT for uploading files to IPFS
3 changes: 2 additions & 1 deletion packages/plugin-story/package.json
Original file line number Diff line number Diff line change
@@ -9,7 +9,8 @@
"@ai16z/plugin-trustdb": "workspace:*",
"@story-protocol/core-sdk": "1.2.0-rc.3",
"tsup": "8.3.5",
"viem": "2.21.54"
"viem": "2.21.54",
"@pinata/sdk": "^2.1.0"
},
"scripts": {
"build": "tsup --format esm --dts",
153 changes: 153 additions & 0 deletions packages/plugin-story/src/actions/attachTerms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import {
composeContext,
elizaLogger,
generateObjectDEPRECATED,
HandlerCallback,
ModelClass,
type IAgentRuntime,
type Memory,
type State,
} from "@ai16z/eliza";
import { storyWalletProvider, WalletProvider } from "../providers/wallet";
import { attachTermsTemplate } from "../templates";
import {
AttachLicenseTermsResponse,
LicenseTerms,
RegisterPILResponse,
} from "@story-protocol/core-sdk";
import { AttachTermsParams } from "../types";
import { zeroAddress } from "viem";

export { attachTermsTemplate };

export class AttachTermsAction {
constructor(private walletProvider: WalletProvider) {}

async attachTerms(params: AttachTermsParams): Promise<{
attachTermsResponse: AttachLicenseTermsResponse;
registerPilTermsResponse: RegisterPILResponse;
}> {
const storyClient = this.walletProvider.getStoryClient();

console.log("params", params);

const licenseTerms: LicenseTerms = {
transferable: true,
royaltyPolicy: params.commercialUse
? "0x28b4F70ffE5ba7A26aEF979226f77Eb57fb9Fdb6"
: zeroAddress,
defaultMintingFee: params.mintingFee
? BigInt(params.mintingFee)
: BigInt(0),
expiration: BigInt(0),
commercialUse: params.commercialUse || false,
commercialAttribution: false,
commercializerChecker: zeroAddress,
commercializerCheckerData: zeroAddress,
commercialRevShare: params.commercialUse
? params.commercialRevShare
: 0,
commercialRevCeiling: BigInt(0),
derivativesAllowed: true,
derivativesAttribution: true,
derivativesApproval: false,
derivativesReciprocal: true,
derivativeRevCeiling: BigInt(0),
currency: "0xC0F6E387aC0B324Ec18EAcf22EE7271207dCE3d5",
uri: "",
};

const registerPilTermsResponse =
await storyClient.license.registerPILTerms({
...licenseTerms,
txOptions: { waitForTransaction: true },
});

const attachTermsResponse =
await storyClient.license.attachLicenseTerms({
ipId: params.ipId,
licenseTermsId: registerPilTermsResponse.licenseTermsId,
txOptions: { waitForTransaction: true },
});

return { attachTermsResponse, registerPilTermsResponse };
}
}

export const attachTermsAction = {
name: "ATTACH_TERMS",
description: "Attach license terms to an IP Asset on Story",
handler: async (
runtime: IAgentRuntime,
message: Memory,
state: State,
options: any,
callback?: HandlerCallback
): Promise<boolean> => {
elizaLogger.log("Starting ATTACH_TERMS handler...");

// initialize or update state
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}

const walletInfo = await storyWalletProvider.get(
runtime,
message,
state
);
state.walletInfo = walletInfo;

const attachTermsContext = composeContext({
state,
template: attachTermsTemplate,
});

const content = await generateObjectDEPRECATED({
runtime,
context: attachTermsContext,
modelClass: ModelClass.SMALL,
});

const walletProvider = new WalletProvider(runtime);
const action = new AttachTermsAction(walletProvider);
try {
const response = await action.attachTerms(content);
// if license terms were attached
if (response.attachTermsResponse.success) {
callback?.({
text: `Successfully attached license terms: ${response.registerPilTermsResponse.licenseTermsId}\nTransaction Hash: ${response.attachTermsResponse.txHash}`,
});
return true;
}
// if license terms were already attached
callback?.({
text: `License terms ${response.registerPilTermsResponse.licenseTermsId} were already attached to IP Asset ${content.ipId}`,
});
return true;
} catch (e) {
elizaLogger.error("Error licensing IP:", e.message);
callback?.({ text: `Error licensing IP: ${e.message}` });
return false;
}
},
template: attachTermsTemplate,
validate: async (runtime: IAgentRuntime) => {
const privateKey = runtime.getSetting("STORY_PRIVATE_KEY");
return typeof privateKey === "string" && privateKey.startsWith("0x");
},
examples: [
[
{
user: "user",
content: {
text: "Attach commercial, 10% rev share license terms to IP Asset 0x2265F2b8e47F98b3Bdf7a1937EAc27282954A4Db",
action: "ATTACH_TERMS",
},
},
],
],
similes: ["ATTACH_TERMS", "ATTACH_TERMS_TO_IP"],
};
18 changes: 9 additions & 9 deletions packages/plugin-story/src/actions/licenseIP.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@ import {
type Memory,
type State,
} from "@ai16z/eliza";
import { WalletProvider } from "../providers/wallet";
import { storyWalletProvider, WalletProvider } from "../providers/wallet";
import { licenseIPTemplate } from "../templates";
import { LicenseIPParams } from "../types";
import { MintLicenseTokensResponse } from "@story-protocol/core-sdk";
@@ -37,7 +37,7 @@ export class LicenseIPAction {
const response = await storyClient.license.mintLicenseTokens({
licensorIpId: params.licensorIpId,
licenseTermsId: params.licenseTermsId,
amount: params.amount,
amount: params.amount || 1,
txOptions: { waitForTransaction: true },
});

@@ -64,6 +64,13 @@ export const licenseIPAction = {
state = await runtime.updateRecentMessageState(state);
}

const walletInfo = await storyWalletProvider.get(
runtime,
message,
state
);
state.walletInfo = walletInfo;

const licenseIPContext = composeContext({
state,
template: licenseIPTemplate,
@@ -96,13 +103,6 @@ export const licenseIPAction = {
},
examples: [
[
{
user: "assistant",
content: {
text: "Ill help you license an IP Asset 0x2265F2b8e47F98b3Bdf7a1937EAc27282954A4Db with license terms 1",
action: "LICENSE_IP",
},
},
{
user: "user",
content: {
69 changes: 54 additions & 15 deletions packages/plugin-story/src/actions/registerIP.ts
Original file line number Diff line number Diff line change
@@ -8,25 +8,63 @@ import {
type Memory,
type State,
} from "@ai16z/eliza";
import { WalletProvider } from "../providers/wallet";
import { storyWalletProvider, WalletProvider } from "../providers/wallet";
import { registerIPTemplate } from "../templates";
import { RegisterIPParams } from "../types";
import { RegisterIpResponse } from "@story-protocol/core-sdk";
import { PinataProvider } from "../providers/pinata";
import { createHash } from "crypto";

export { registerIPTemplate };

export class RegisterIPAction {
constructor(private walletProvider: WalletProvider) {}
constructor(
private walletProvider: WalletProvider,
private pinataProvider: PinataProvider
) {}

async registerIP(params: RegisterIPParams): Promise<RegisterIpResponse> {
const storyClient = this.walletProvider.getStoryClient();

const response = await storyClient.ipAsset.register({
nftContract: params.contractAddress,
tokenId: params.tokenId,
txOptions: { waitForTransaction: true },
// configure ip metadata
const ipMetadata = storyClient.ipAsset.generateIpMetadata({
title: params.title,
description: params.description,
ipType: params.ipType ? params.ipType : undefined,
});

// configure nft metadata
const nftMetadata = {
name: params.title,
description: params.description,
};

// upload metadata to ipfs
const ipIpfsHash =
await this.pinataProvider.uploadJSONToIPFS(ipMetadata);
const ipHash = createHash("sha256")
.update(JSON.stringify(ipMetadata))
.digest("hex");
const nftIpfsHash =
await this.pinataProvider.uploadJSONToIPFS(nftMetadata);
const nftHash = createHash("sha256")
.update(JSON.stringify(nftMetadata))
.digest("hex");

// register ip
const response =
await storyClient.ipAsset.mintAndRegisterIpAssetWithPilTerms({
spgNftContract: "0xC81B2cbEFD1aA0227bf513729580d3CF40fd61dF",
terms: [],
ipMetadata: {
ipMetadataURI: `https://ipfs.io/ipfs/${ipIpfsHash}`,
ipMetadataHash: `0x${ipHash}`,
nftMetadataURI: `https://ipfs.io/ipfs/${nftIpfsHash}`,
nftMetadataHash: `0x${nftHash}`,
},
txOptions: { waitForTransaction: true },
});

return response;
}
}
@@ -50,6 +88,13 @@ export const registerIPAction = {
state = await runtime.updateRecentMessageState(state);
}

const walletInfo = await storyWalletProvider.get(
runtime,
message,
state
);
state.walletInfo = walletInfo;

const registerIPContext = composeContext({
state,
template: registerIPTemplate,
@@ -62,7 +107,8 @@ export const registerIPAction = {
});

const walletProvider = new WalletProvider(runtime);
const action = new RegisterIPAction(walletProvider);
const pinataProvider = new PinataProvider(runtime);
const action = new RegisterIPAction(walletProvider, pinataProvider);
try {
const response = await action.registerIP(content);
callback?.({
@@ -82,17 +128,10 @@ export const registerIPAction = {
},
examples: [
[
{
user: "assistant",
content: {
text: "Ill help you register an NFT with contract address 0x041B4F29183317Fd352AE57e331154b73F8a1D73 and token id 209 as IP",
action: "REGISTER_IP",
},
},
{
user: "user",
content: {
text: "Register an NFT with contract address 0x041B4F29183317Fd352AE57e331154b73F8a1D73 and token id 209 as IP",
text: "Register my IP titled 'My IP' with the description 'This is my IP'",
action: "REGISTER_IP",
},
},
8 changes: 6 additions & 2 deletions packages/plugin-story/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
export * from "./actions/registerIP";
export * from "./actions/licenseIP";
export * from "./actions/attachTerms";
export * from "./providers/wallet";
export * from "./providers/pinata";
export * from "./types";

import type { Plugin } from "@ai16z/eliza";
import { storyWalletProvider } from "./providers/wallet";
import { storyPinataProvider } from "./providers/pinata";
import { registerIPAction } from "./actions/registerIP";
import { licenseIPAction } from "./actions/licenseIP";
import { attachTermsAction } from "./actions/attachTerms";

export const storyPlugin: Plugin = {
name: "story",
description: "Story integration plugin",
providers: [storyWalletProvider],
providers: [storyWalletProvider, storyPinataProvider],
evaluators: [],
services: [],
actions: [registerIPAction, licenseIPAction],
actions: [registerIPAction, licenseIPAction, attachTermsAction],
};

export default storyPlugin;
Loading