Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 24 additions & 1 deletion packages/core-sdk/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import { PermissionClient } from "./resources/permission";
import { LicenseClient } from "./resources/license";
import { DisputeClient } from "./resources/dispute";
import { IPAccountClient } from "./resources/ipAccount";
import { chain, chainStringToViemChain } from "./utils/utils";
import { chain, chainStringToViemChain, validateAddress } from "./utils/utils";
import { RoyaltyClient } from "./resources/royalty";
import { NftClient } from "./resources/nftClient";
import { GroupClient } from "./resources/group";
import { SimpleWalletClient } from "./abi/generated";
import { WipClient } from "./resources/wip";

if (typeof process !== "undefined") {
dotenv.config();
Expand All @@ -36,6 +37,7 @@ export class StoryClient {
private _royalty: RoyaltyClient | null = null;
private _nftClient: NftClient | null = null;
private _group: GroupClient | null = null;
private _wip: WipClient | null = null;

/**
* @param config - the configuration for the SDK client
Expand Down Expand Up @@ -218,4 +220,25 @@ export class StoryClient {

return this._group;
}

public get wipClient(): WipClient {
if (this._wip === null) {
this._wip = new WipClient(this.rpcClient, this.wallet);
}
return this._wip;
}

public async walletBalance(): Promise<bigint> {
if (!this.wallet.account) {
throw new Error("No account found in wallet");
}
return await this.getBalance(this.wallet.account.address);
}

public async getBalance(address: string): Promise<bigint> {
const validAddress = validateAddress(address);
return await this.rpcClient.getBalance({
address: validAddress,
});
}
}
7 changes: 5 additions & 2 deletions packages/core-sdk/src/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ import { Hex } from "viem";
export const AddressZero = "0x0000000000000000000000000000000000000000";
export const HashZero = "0x0000000000000000000000000000000000000000000000000000000000000000";
export const defaultFunctionSelector: Hex = "0x00000000";
export const royaltySharesTotalSupply: number = 100000000;
export const MAX_ROYALTY_TOKEN = 100000000;
export const royaltySharesTotalSupply: number = 100_000_000;
export const MAX_ROYALTY_TOKEN = 100_000_000;

/** Address for the WIP contract. This address is fixed */
export const WIP_TOKEN_ADDRESS = "0x1514000000000000000000000000000000000000";
100 changes: 100 additions & 0 deletions packages/core-sdk/src/resources/wip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Address, Hex, PublicClient, WriteContractParameters } from "viem";

import { handleError } from "../utils/errors";
import { Erc20TokenClient, SimpleWalletClient, erc20TokenAbi } from "../abi/generated";
import { validateAddress } from "../utils/utils";
import { WIP_TOKEN_ADDRESS } from "../constants/common";
import { ApproveRequest, DepositRequest, WithdrawRequest } from "../types/resources/wip";
import { handleTxOptions } from "../utils/txOptions";

export class WipClient {
public wipClient: Erc20TokenClient;
private readonly rpcClient: PublicClient;
private readonly wallet: SimpleWalletClient;

constructor(rpcClient: PublicClient, wallet: SimpleWalletClient) {
this.wipClient = new Erc20TokenClient(rpcClient, wallet, WIP_TOKEN_ADDRESS);
this.rpcClient = rpcClient;
this.wallet = wallet;
}

/**
* Wraps the selected amount of IP to WIP.
* The WIP will be deposited to the wallet that transferred the IP.
*/
public async deposit({ amount, txOptions }: DepositRequest) {
try {
if (amount <= 0) {
throw new Error("WIP deposit amount must be greater than 0.");
}
const { request: call } = await this.rpcClient.simulateContract({
abi: erc20TokenAbi,
address: WIP_TOKEN_ADDRESS,
functionName: "deposit",
account: this.wallet.account,
value: BigInt(amount),
});
const txHash = await this.wallet.writeContract(call as WriteContractParameters);
return handleTxOptions({
txHash,
txOptions,
rpcClient: this.rpcClient,
});
} catch (error) {
handleError(error, "Failed to deposit IP for WIP");
}
}

/**
* Unwraps the selected amount of WIP to IP.
*/
public async withdraw({ amount, txOptions }: WithdrawRequest) {
try {
const targetAmt = BigInt(amount);
if (targetAmt <= 0) {
throw new Error("WIP withdraw amount must be greater than 0.");
}
const txHash = await this.wipClient.withdraw({ value: targetAmt });
return handleTxOptions({
txHash,
txOptions,
rpcClient: this.rpcClient,
});
} catch (error) {
handleError(error, "Failed to withdraw WIP");
}
}

/**
* Approve a spender to use the wallet's WIP balance.
*/
public async approve(req: ApproveRequest): Promise<{ txHash: Hex }> {
try {
const amount = BigInt(req.amount);
if (amount <= 0) {
throw new Error("WIP approve amount must be greater than 0.");
}
const spender = validateAddress(req.spender);
const txHash = await this.wipClient.approve({
spender,
amount,
});
return handleTxOptions({
txHash,
txOptions: req.txOptions,
rpcClient: this.rpcClient,
});
} catch (error) {
handleError(error, "Failed to approve WIP");
}
}

/**
* Returns the balance of WIP for an address.
*/
public async balanceOf(addr: Address): Promise<bigint> {
const owner = validateAddress(addr);
const ret = await this.wipClient.balanceOf({ owner });
return ret.result;
}
}
11 changes: 8 additions & 3 deletions packages/core-sdk/src/types/common.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { Address, Hex } from "viem";

import { TxOptions } from "./options";
import { WithTxOptions } from "./options";
import { IpMetadataForWorkflow } from "../utils/getIpMetadataForWorkflow";

export type TypedData = {
interface: string;
data: unknown[];
};

export type IpMetadataAndTxOption = {
export type IpMetadataAndTxOptions = WithTxOptions & {
ipMetadata?: Partial<IpMetadataForWorkflow>;
txOptions?: TxOptions;
};

export type LicensingConfig = {
Expand All @@ -29,3 +28,9 @@ export type InnerLicensingConfig = {
commercialRevShare: number;
expectMinimumGroupRewardShare: number;
} & LicensingConfig;

/**
* Input for token amount, can be bigint or number.
* Will be converted to bigint for contract calls.
*/
export type TokenAmountInput = bigint | number;
2 changes: 1 addition & 1 deletion packages/core-sdk/src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ export type TxOptions = Omit<WaitForTransactionReceiptParameters, "hash"> & {
encodedTxDataOnly?: boolean;
};

export type WithTxOptions<T> = T & {
export type WithTxOptions = {
txOptions?: TxOptions;
};
19 changes: 19 additions & 0 deletions packages/core-sdk/src/types/resources/wip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Address } from "viem";

import { TokenAmountInput } from "../common";
import { WithTxOptions } from "../options";

export type ApproveRequest = WithTxOptions & {
/** The address that will use the WIP tokens */
spender: Address;
/** The amount of WIP tokens to approve. */
amount: TokenAmountInput;
};

export type DepositRequest = WithTxOptions & {
amount: TokenAmountInput;
};

export type WithdrawRequest = WithTxOptions & {
amount: TokenAmountInput;
};
16 changes: 16 additions & 0 deletions packages/core-sdk/src/types/utils/txOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Hex, PublicClient, TransactionReceipt } from "viem";

import { TxOptions } from "../options";

export type HandleTxOptionsParams = {
txHash: Hex;
txOptions?: TxOptions;
rpcClient: PublicClient;
};

export type HandleTxOptionsResponse = {
txHash: Hex;

/** Transaction receipt, only available if waitForTransaction is set to true */
receipt?: TransactionReceipt;
};
16 changes: 16 additions & 0 deletions packages/core-sdk/src/utils/txOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HandleTxOptionsParams, HandleTxOptionsResponse } from "../types/utils/txOptions";

export async function handleTxOptions({
txOptions,
rpcClient,
txHash,
}: HandleTxOptionsParams): Promise<HandleTxOptionsResponse> {
if (!txOptions || !txOptions.waitForTransaction) {
return { txHash };
}
const receipt = await rpcClient.waitForTransactionReceipt({
...txOptions,
hash: txHash,
});
return { txHash, receipt };
}
53 changes: 53 additions & 0 deletions packages/core-sdk/test/integration/wip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import chai from "chai";
import chaiAsPromised from "chai-as-promised";
import { parseEther } from "viem";
import { StoryClient } from "../../src";
import { getStoryClient, TEST_WALLET_ADDRESS } from "./utils/util";

chai.use(chaiAsPromised);
const expect = chai.expect;

describe("WIP Functions", () => {
let client: StoryClient;

before(async () => {
client = getStoryClient();
});

describe("deposit", () => {
const ipAmtStr = "0.01";
const ipAmt = parseEther(ipAmtStr);

it(`should deposit ${ipAmtStr} WIP`, async () => {
const balanceBefore = await client.walletBalance();
const wipBefore = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS);
const rsp = await client.wipClient.deposit({
amount: ipAmt,
txOptions: { waitForTransaction: true },
});
expect(rsp.txHash).to.be.a("string");
const balanceAfter = await client.walletBalance();
const wipAfter = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS);
expect(wipAfter).to.equal(wipBefore + ipAmt);
const gasCost = rsp.receipt!.gasUsed * rsp.receipt!.effectiveGasPrice;
expect(balanceAfter).to.equal(balanceBefore - ipAmt - gasCost);
});
});

describe("withdraw", () => {
it("should withdrawal WIP", async () => {
const balanceBefore = await client.walletBalance();
const wipBefore = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS);
const rsp = await client.wipClient.withdraw({
amount: wipBefore,
txOptions: { waitForTransaction: true },
});
expect(rsp.txHash).to.be.a("string");
const wipAfter = await client.wipClient.balanceOf(TEST_WALLET_ADDRESS);
expect(wipAfter).to.equal(0n);
const balanceAfter = await client.walletBalance();
const gasCost = rsp.receipt!.gasUsed * rsp.receipt!.effectiveGasPrice;
expect(balanceAfter).to.equal(balanceBefore + wipBefore - gasCost);
});
});
});