diff --git a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol index 98a931b9b4..e6c3e542a0 100644 --- a/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol +++ b/packages/ethereum-contracts/contracts/utils/SuperfluidFrameworkDeploymentSteps.sol @@ -54,6 +54,9 @@ contract SuperfluidFrameworkDeploymentSteps { InstantDistributionAgreementV1 ida; IDAv1Library.InitData idaLib; SuperTokenFactory superTokenFactory; + ISuperToken superTokenLogic; + ConstantOutflowNFT constantOutflowNFT; + ConstantInflowNFT constantInflowNFT; TestResolver resolver; SuperfluidLoader superfluidLoader; CFAv1Forwarder cfaV1Forwarder; @@ -306,6 +309,9 @@ contract SuperfluidFrameworkDeploymentSteps { ida: idaV1, idaLib: IDAv1Library.InitData(host, idaV1), superTokenFactory: superTokenFactory, + superTokenLogic: superTokenLogic, + constantOutflowNFT: constantOutflowNFT, + constantInflowNFT: constantInflowNFT, resolver: testResolver, superfluidLoader: superfluidLoader, cfaV1Forwarder: cfaV1Forwarder, diff --git a/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js b/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js index 8395559418..6604b36c6d 100644 --- a/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js +++ b/packages/ethereum-contracts/dev-scripts/run-deploy-contracts-and-token.js @@ -5,6 +5,7 @@ deployContractsAndToken() .then(async ({deployer, tokenDeploymentOutput}) => { const frameworkAddresses = await deployer.getFramework(); + const deploymentOutput = { network: "mainnet", testNetwork: "hardhat", @@ -17,6 +18,8 @@ deployContractsAndToken() nativeAssetSuperTokenAddress: tokenDeploymentOutput.nativeAssetSuperTokenData .nativeAssetSuperTokenAddress, + constantOutflowNFTAddress: frameworkAddresses.constantOutflowNFT, + constantInflowNFTAddress: frameworkAddresses.constantInflowNFT, }; // create json output diff --git a/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol b/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol index be87bc70a7..4df2f141d7 100644 --- a/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol +++ b/packages/ethereum-contracts/test/foundry/SuperfluidFrameworkDeployer.t.sol @@ -2,12 +2,7 @@ pragma solidity 0.8.19; import { FoundrySuperfluidTester } from "./FoundrySuperfluidTester.sol"; -import { - IPureSuperToken, - ISETH, - TestToken, - SuperToken -} from "../../contracts/utils/SuperfluidFrameworkDeployer.sol"; +import { IPureSuperToken, ISETH, TestToken, SuperToken } from "../../contracts/utils/SuperfluidFrameworkDeployer.sol"; import { SuperfluidLoader } from "../../contracts/utils/SuperfluidLoader.sol"; contract SuperfluidFrameworkDeployerTest is FoundrySuperfluidTester { @@ -19,13 +14,18 @@ contract SuperfluidFrameworkDeployerTest is FoundrySuperfluidTester { assertTrue(address(sf.cfa) != address(0), "SFDeployer: cfa not deployed"); assertTrue(address(sf.ida) != address(0), "SFDeployer: ida not deployed"); assertTrue(address(sf.superTokenFactory) != address(0), "SFDeployer: superTokenFactory not deployed"); + assertTrue(address(sf.superTokenLogic) != address(0), "SFDeployer: superTokenLogic not deployed"); + assertTrue(address(sf.constantOutflowNFT) != address(0), "SFDeployer: constantOutflowNFT not deployed"); + assertTrue(address(sf.constantInflowNFT) != address(0), "SFDeployer: constantInflowNFT not deployed"); assertTrue(address(sf.resolver) != address(0), "SFDeployer: resolver not deployed"); assertTrue(address(sf.superfluidLoader) != address(0), "SFDeployer: superfluidLoader not deployed"); assertTrue(address(sf.cfaV1Forwarder) != address(0), "SFDeployer: cfaV1Forwarder not deployed"); } function testResolverGetsGovernance() public { - assertEq(sf.resolver.get("TestGovernance.test"), address(sf.governance), "SFDeployer: governance not registered"); + assertEq( + sf.resolver.get("TestGovernance.test"), address(sf.governance), "SFDeployer: governance not registered" + ); } function testResolverGetsHost() public { @@ -74,7 +74,8 @@ contract SuperfluidFrameworkDeployerTest is FoundrySuperfluidTester { assertEq(_superToken.symbol(), string.concat(_symbol, "x"), "SFDeployer: Super token symbol not properly set"); // assert proper resolver listing for underlying and wrapper super token - address resolverUnderlyingTokenAddress = sf.resolver.get(string.concat("tokens.test.", underlyingToken.symbol())); + address resolverUnderlyingTokenAddress = + sf.resolver.get(string.concat("tokens.test.", underlyingToken.symbol())); assertEq( resolverUnderlyingTokenAddress, address(underlyingToken), @@ -94,7 +95,8 @@ contract SuperfluidFrameworkDeployerTest is FoundrySuperfluidTester { ); // assert proper resolver listing - address resolverTokenAddress = sf.resolver.get(string.concat("supertokens.test.", nativeAssetSuperToken.symbol())); + address resolverTokenAddress = + sf.resolver.get(string.concat("supertokens.test.", nativeAssetSuperToken.symbol())); assertEq( resolverTokenAddress, address(nativeAssetSuperToken), diff --git a/packages/sdk-core/CHANGELOG.md b/packages/sdk-core/CHANGELOG.md index c600b90b1e..2272bec2a7 100644 --- a/packages/sdk-core/CHANGELOG.md +++ b/packages/sdk-core/CHANGELOG.md @@ -5,6 +5,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added +- Support for `ConstantOutflowNFT` and `ConstantInflowNFT` functions + ## [0.6.9] - 2023-09-11 ### Added diff --git a/packages/sdk-core/src/ConstantInflowNFT.ts b/packages/sdk-core/src/ConstantInflowNFT.ts new file mode 100644 index 0000000000..85abb311d8 --- /dev/null +++ b/packages/sdk-core/src/ConstantInflowNFT.ts @@ -0,0 +1,18 @@ +import { ethers } from "ethers"; + +import FlowNFTBase from "./FlowNFTBase"; +import { + ConstantInflowNFT__factory, + IConstantInflowNFT, +} from "./typechain-types"; + +export default class ConstantInflowNFT extends FlowNFTBase { + override readonly contract: IConstantInflowNFT; + constructor(address: string) { + super(address); + this.contract = new ethers.Contract( + address, + ConstantInflowNFT__factory.abi + ) as IConstantInflowNFT; + } +} diff --git a/packages/sdk-core/src/ConstantOutflowNFT.ts b/packages/sdk-core/src/ConstantOutflowNFT.ts new file mode 100644 index 0000000000..e081da27df --- /dev/null +++ b/packages/sdk-core/src/ConstantOutflowNFT.ts @@ -0,0 +1,18 @@ +import { ethers } from "ethers"; + +import FlowNFTBase from "./FlowNFTBase"; +import { + ConstantOutflowNFT__factory, + IConstantOutflowNFT, +} from "./typechain-types"; + +export default class ConstantOutflowNFT extends FlowNFTBase { + override readonly contract: IConstantOutflowNFT; + constructor(address: string) { + super(address); + this.contract = new ethers.Contract( + address, + ConstantOutflowNFT__factory.abi + ) as IConstantOutflowNFT; + } +} diff --git a/packages/sdk-core/src/ERC20Token.ts b/packages/sdk-core/src/ERC20Token.ts index 33a0974548..0bd27eb052 100644 --- a/packages/sdk-core/src/ERC20Token.ts +++ b/packages/sdk-core/src/ERC20Token.ts @@ -2,7 +2,13 @@ import { ethers } from "ethers"; import Operation from "./Operation"; import { SFError } from "./SFError"; -import { IBaseSuperTokenParams, ITransferFromParams } from "./interfaces"; +import { + ERC20AllowanceParams, + ERC20BalanceOfParams, + IBaseSuperTokenParams, + ITransferFromParams, + ProviderOrSigner, +} from "./interfaces"; import { IERC20Metadata, IERC20Metadata__factory } from "./typechain-types"; import { normalizeAddress } from "./utils"; @@ -32,11 +38,7 @@ export default class ERC20Token { owner, spender, providerOrSigner, - }: { - owner: string; - spender: string; - providerOrSigner: ethers.providers.Provider | ethers.Signer; - }): Promise => { + }: ERC20AllowanceParams): Promise => { const normalizedOwner = normalizeAddress(owner); const normalizedSpender = normalizeAddress(spender); try { @@ -62,10 +64,7 @@ export default class ERC20Token { balanceOf = async ({ account, providerOrSigner, - }: { - account: string; - providerOrSigner: ethers.providers.Provider | ethers.Signer; - }): Promise => { + }: ERC20BalanceOfParams): Promise => { try { const normalizedAccount = normalizeAddress(account); const balanceOf = await this.contract @@ -89,7 +88,7 @@ export default class ERC20Token { name = async ({ providerOrSigner, }: { - providerOrSigner: ethers.providers.Provider | ethers.Signer; + providerOrSigner: ProviderOrSigner; }): Promise => { try { const name = await this.contract.connect(providerOrSigner).name(); @@ -111,7 +110,7 @@ export default class ERC20Token { symbol = async ({ providerOrSigner, }: { - providerOrSigner: ethers.providers.Provider | ethers.Signer; + providerOrSigner: ProviderOrSigner; }): Promise => { try { const symbol = await this.contract @@ -135,7 +134,7 @@ export default class ERC20Token { totalSupply = async ({ providerOrSigner, }: { - providerOrSigner: ethers.providers.Provider | ethers.Signer; + providerOrSigner: ProviderOrSigner; }): Promise => { try { const totalSupply = await this.contract diff --git a/packages/sdk-core/src/ERC721Token.ts b/packages/sdk-core/src/ERC721Token.ts new file mode 100644 index 0000000000..f7feee9647 --- /dev/null +++ b/packages/sdk-core/src/ERC721Token.ts @@ -0,0 +1,310 @@ +import { ethers } from "ethers"; + +import Operation from "./Operation"; +import { SFError } from "./SFError"; +import { + ERC721ApproveParams, + ERC721BalanceOfParams, + ERC721GetApprovedParams, + ERC721IsApprovedForAllParams, + ERC721OwnerOfParams, + ERC721SafeTransferFromParams, + ERC721SetApprovalForAllParams, + ERC721TokenURIParams, + ERC721TransferFromParams, + NFTFlowData, + ProviderOrSigner, +} from "./interfaces"; +import { + IERC721Metadata, + IERC721Metadata__factory, + IFlowNFTBase, +} from "./typechain-types"; +import { getSanitizedTimestamp, normalizeAddress } from "./utils"; + +export default class ERC721MetadataToken { + readonly address: string; + readonly contract: IERC721Metadata; + + constructor(address: string) { + this.address = address; + + this.contract = new ethers.Contract( + address, + IERC721Metadata__factory.abi + ) as IERC721Metadata; + } + + /** ### ERC721 Token Contract Read Functions ### */ + + /** + * Returns the ERC721 balanceOf the `owner`. + * @param owner the owner you would like to query + * @param providerOrSigner a provider or signer for executing a web3 call + * @returns {Promise} the token balance of `owner` + */ + balanceOf = async (params: ERC721BalanceOfParams): Promise => { + try { + const normalizedOwner = normalizeAddress(params.owner); + const balanceOf = await this.contract + .connect(params.providerOrSigner) + .balanceOf(normalizedOwner); + return balanceOf.toString(); + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting balanceOf", + cause: err, + }); + } + }; + + /** + * Returns the owner of the NFT specified by `tokenId`. + * NOTE: Throws if `tokenId` is not a valid NFT. + * @param tokenId the token id + * @param providerOrSigner a provider or signer for executing a web3 call + * @returns {string} the address of the owner of the NFT + */ + ownerOf = async (params: ERC721OwnerOfParams): Promise => { + try { + const ownerOf = await this.contract + .connect(params.providerOrSigner) + .ownerOf(params.tokenId); + return ownerOf.toString(); + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting ownerOf", + cause: err, + }); + } + }; + + /** + * Returns the approved address for a single NFT, or the zero address if there is none. + * @param tokenId the token id + * @param providerOrSigner a provider or signer for executing a web3 call + * @returns {string} the approved address for this NFT, or the zero address if there is none + */ + getApproved = async (params: ERC721GetApprovedParams): Promise => { + try { + const approved = await this.contract + .connect(params.providerOrSigner) + .getApproved(params.tokenId); + return approved; + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting getApproved", + cause: err, + }); + } + }; + + /** + * Returns whether `operator` is approved for all of `owner`'s NFTs. + * @param owner the owner of NFTs + * @param operator an operator for the owner's NFTs + * @param providerOrSigner a provider or signer for executing a web3 call + * @returns {bool} + */ + isApprovedForAll = async ( + params: ERC721IsApprovedForAllParams + ): Promise => { + try { + const normalizedOwner = normalizeAddress(params.owner); + const normalizedOperator = normalizeAddress(params.operator); + const approved = await this.contract + .connect(params.providerOrSigner) + .isApprovedForAll(normalizedOwner, normalizedOperator); + return approved; + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting isApprovedForAll", + cause: err, + }); + } + }; + + /** + * Returns the token name + * @param providerOrSigner a provider or signer for executing a web3 call + * @returns {string} the token name + */ + name = async ({ + providerOrSigner, + }: { + providerOrSigner: ProviderOrSigner; + }): Promise => { + try { + const name = await this.contract.connect(providerOrSigner).name(); + return name; + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting name", + cause: err, + }); + } + }; + + /** + * Returns the token symbol + * @param providerOrSigner a provider or signer for executing a web3 call + * @returns {string} the token symbol + */ + symbol = async ({ + providerOrSigner, + }: { + providerOrSigner: ProviderOrSigner; + }): Promise => { + try { + const symbol = await this.contract + .connect(providerOrSigner) + .symbol(); + return symbol; + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting symbol", + cause: err, + }); + } + }; + + /** + * Returns the token URI + * @param tokenId the token id + * @returns {string} + */ + tokenURI = async (params: ERC721TokenURIParams): Promise => { + try { + const uri = await this.contract + .connect(params.providerOrSigner) + .tokenURI(params.tokenId); + return uri; + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting tokenURI", + cause: err, + }); + } + }; + + /** ### ERC721 Token Contract Write Functions ### */ + + /** + * Approve `approved` to spend `tokenId` NFT. + * @param approved The receiver approved. + * @param tokenId The tokenId approved. + * @param overrides ethers overrides object for more control over the transaction sent. + * @returns {Operation} An instance of Operation which can be executed. + */ + approve = (params: ERC721ApproveParams): Operation => { + const normalizedReceiver = normalizeAddress(params.approved); + const txn = this.contract.populateTransaction.approve( + normalizedReceiver, + params.tokenId, + params.overrides || {} + ); + return new Operation(txn, "UNSUPPORTED"); + }; + + /** + * Approve `operator` to spend all NFTs of the signer (`msg.sender`). + * @param operator The operator approved. + * @param approved The approved status. + * @returns {Operation} An instance of Operation which can be executed. + */ + setApprovalForAll = (params: ERC721SetApprovalForAllParams): Operation => { + const normalizedOperator = normalizeAddress(params.operator); + const txn = this.contract.populateTransaction.setApprovalForAll( + normalizedOperator, + params.approved, + params.overrides || {} + ); + return new Operation(txn, "UNSUPPORTED"); + }; + + /** + * Transfer `tokenId` from `from` to `to` . + * @param from The owner of the NFT. + * @param to The receiver of the NFT. + * @param tokenId The token to be transferred. + * @param overrides ethers overrides object for more control over the transaction sent. + * @returns {Operation} An instance of Operation which can be executed. + */ + transferFrom = (params: ERC721TransferFromParams): Operation => { + const normalizedFrom = normalizeAddress(params.from); + const normalizedTo = normalizeAddress(params.to); + const txn = this.contract.populateTransaction.transferFrom( + normalizedFrom, + normalizedTo, + params.tokenId, + params.overrides || {} + ); + return new Operation(txn, "UNSUPPORTED"); + }; + + /** + * Safe transfer `tokenId` from `from` to `to` (see IERC721.sol OZ Natspec for more details). + * Data is empty in this version of safeTransferFrom. + * @param from The owner of the NFT. + * @param to The receiver of the NFT. + * @param tokenId The token to be transferred. + * @param overrides ethers overrides object for more control over the transaction sent. + * @returns {Operation} An instance of Operation which can be executed. + */ + safeTransferFrom = (params: ERC721TransferFromParams): Operation => { + const normalizedFrom = normalizeAddress(params.from); + const normalizedTo = normalizeAddress(params.to); + const txn = this.contract.populateTransaction[ + "safeTransferFrom(address,address,uint256)" + ](normalizedFrom, normalizedTo, params.tokenId, params.overrides || {}); + return new Operation(txn, "UNSUPPORTED"); + }; + + /** + * Safe transfer `tokenId` from `from` to `to` with `data`. + * @param from The owner of the NFT. + * @param to The receiver of the NFT. + * @param tokenId The token to be transferred. + * @param data The data to be sent with the safe transfer check. + * @param overrides ethers overrides object for more control over the transaction sent. + * @returns {Operation} An instance of Operation which can be executed. + */ + safeTransferFromWithData = ( + params: ERC721SafeTransferFromParams + ): Operation => { + const normalizedFrom = normalizeAddress(params.from); + const normalizedTo = normalizeAddress(params.to); + const txn = this.contract.populateTransaction[ + "safeTransferFrom(address,address,uint256,bytes)" + ]( + normalizedFrom, + normalizedTo, + params.tokenId, + params.data, + params.overrides || {} + ); + return new Operation(txn, "UNSUPPORTED"); + }; + + /** + * Sanitizes NFTFlowData, converting number to Date. + * @param params NFTFlowData + * @returns {NFTFlowData} sanitized NFTFlowData + */ + _sanitizeNFTFlowData = ( + params: IFlowNFTBase.FlowNFTDataStructOutput + ): NFTFlowData => { + return { + flowSender: params.flowSender, + flowStartDate: getSanitizedTimestamp(params.flowStartDate), + flowReceiver: params.flowReceiver, + }; + }; +} diff --git a/packages/sdk-core/src/FlowNFTBase.ts b/packages/sdk-core/src/FlowNFTBase.ts new file mode 100644 index 0000000000..9b95fd2872 --- /dev/null +++ b/packages/sdk-core/src/FlowNFTBase.ts @@ -0,0 +1,85 @@ +import { ethers } from "ethers"; + +import ERC721MetadataToken from "./ERC721Token"; +import { SFError } from "./SFError"; +import { NFTFlowData } from "./interfaces"; +import { FlowNFTBase__factory, IFlowNFTBase } from "./typechain-types"; +import { normalizeAddress } from "./utils"; + +export default class FlowNFTBase extends ERC721MetadataToken { + override readonly contract: IFlowNFTBase; + constructor(address: string) { + super(address); + this.contract = new ethers.Contract( + address, + FlowNFTBase__factory.abi + ) as IFlowNFTBase; + } + + /** ### ConstantInflowNFT Contract Read Functions ### */ + + /** + * Returns the computed `tokenId` of a flow NFT given a sender and receiver. + * @param sender the flow sender + * @param receiver the flow receiver + * @returns + */ + getTokenId = async ({ + superToken, + sender, + receiver, + providerOrSigner, + }: { + superToken: string; + sender: string; + receiver: string; + providerOrSigner: ethers.providers.Provider | ethers.Signer; + }): Promise => { + const normalizedSuperToken = normalizeAddress(superToken); + const normalizedSender = normalizeAddress(sender); + const normalizedReceiver = normalizeAddress(receiver); + try { + const tokenId = await this.contract + + .connect(providerOrSigner) + .getTokenId( + normalizedSuperToken, + normalizedSender, + normalizedReceiver + ); + return tokenId.toString(); + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting token id", + cause: err, + }); + } + }; + + /** + * Returns the NFT flow data of the NFT with `tokenId`. + * @param tokenId the token id + * @returns {NFTFlowData} the NFT flow data + */ + flowDataByTokenId = async ({ + tokenId, + providerOrSigner, + }: { + tokenId: string; + providerOrSigner: ethers.providers.Provider | ethers.Signer; + }): Promise => { + try { + const flowData = await this.contract + .connect(providerOrSigner) + .flowDataByTokenId(tokenId); + return this._sanitizeNFTFlowData(flowData); + } catch (err) { + throw new SFError({ + type: "NFT_READ", + message: "There was an error getting flow data by token id", + cause: err, + }); + } + }; +} diff --git a/packages/sdk-core/src/SFError.ts b/packages/sdk-core/src/SFError.ts index 4792355395..c6266ab708 100644 --- a/packages/sdk-core/src/SFError.ts +++ b/packages/sdk-core/src/SFError.ts @@ -5,6 +5,7 @@ export type ErrorType = | "SUPERTOKEN_INITIALIZATION" | "CREATE_SIGNER" | "SUPERTOKEN_READ" + | "NFT_READ" | "CFAV1_READ" | "IDAV1_READ" | "INVALID_ADDRESS" diff --git a/packages/sdk-core/src/SuperToken.ts b/packages/sdk-core/src/SuperToken.ts index 0239e41c95..0cf518de5b 100644 --- a/packages/sdk-core/src/SuperToken.ts +++ b/packages/sdk-core/src/SuperToken.ts @@ -1,6 +1,8 @@ import { BytesLike, ethers, Overrides } from "ethers"; import ConstantFlowAgreementV1 from "./ConstantFlowAgreementV1"; +import ConstantInflowNFT from "./ConstantInflowNFT"; +import ConstantOutflowNFT from "./ConstantOutflowNFT"; import ERC20Token from "./ERC20Token"; import Governance from "./Governance"; import InstantDistributionAgreementV1 from "./InstantDistributionAgreementV1"; @@ -54,6 +56,11 @@ import { normalizeAddress, } from "./utils"; +export interface NFTAddresses { + readonly constantInflowNFTProxy: string; + readonly constantOutflowNFTProxy: string; +} + export interface ITokenSettings { readonly address: string; readonly config: IConfig; @@ -81,6 +88,10 @@ export default abstract class SuperToken extends ERC20Token { readonly idaV1: InstantDistributionAgreementV1; readonly governance: Governance; readonly underlyingToken?: ERC20Token; + readonly constantOutflowNFTProxy?: ConstantOutflowNFT; + readonly constantInflowNFTProxy?: ConstantInflowNFT; + readonly constantOutflowNFTLogic?: string; + readonly constantInflowNFTLogic?: string; override readonly contract: ISuperToken; protected constructor(options: ITokenOptions, settings: ITokenSettings) { @@ -146,21 +157,35 @@ export default abstract class SuperToken extends ERC20Token { const nativeTokenSymbol = resolverData.nativeTokenSymbol || "ETH"; const nativeSuperTokenSymbol = nativeTokenSymbol + "x"; + const constantOutflowNFTProxy = + await superToken.CONSTANT_OUTFLOW_NFT(); + const constantInflowNFTProxy = + await superToken.CONSTANT_INFLOW_NFT(); + const nftAddresses: NFTAddresses = { + constantOutflowNFTProxy, + constantInflowNFTProxy, + }; + if (nativeSuperTokenSymbol === tokenSymbol) { return new NativeAssetSuperToken( options, settings, - nativeTokenSymbol + nativeTokenSymbol, + nftAddresses ); } if (underlyingTokenAddress !== ethers.constants.AddressZero) { - return new WrapperSuperToken(options, { - ...settings, - underlyingTokenAddress, - }); + return new WrapperSuperToken( + options, + { + ...settings, + underlyingTokenAddress, + }, + nftAddresses + ); } - return new PureSuperToken(options, settings); + return new PureSuperToken(options, settings, nftAddresses); } catch (err) { throw new SFError({ type: "SUPERTOKEN_INITIALIZATION", @@ -746,13 +771,22 @@ export default abstract class SuperToken extends ERC20Token { */ export class WrapperSuperToken extends SuperToken { override readonly underlyingToken: ERC20Token; + override readonly constantOutflowNFTProxy: ConstantOutflowNFT; + override readonly constantInflowNFTProxy: ConstantInflowNFT; constructor( options: ITokenOptions, - settings: ITokenSettings & { underlyingTokenAddress: string } + settings: ITokenSettings & { underlyingTokenAddress: string }, + nftAddresses: NFTAddresses ) { super(options, settings); this.underlyingToken = new ERC20Token(settings.underlyingTokenAddress); + this.constantInflowNFTProxy = new ConstantInflowNFT( + nftAddresses.constantInflowNFTProxy + ); + this.constantOutflowNFTProxy = new ConstantOutflowNFT( + nftAddresses.constantOutflowNFTProxy + ); } /** ### WrapperSuperToken Contract Write Functions ### */ @@ -854,8 +888,21 @@ export class WrapperSuperToken extends SuperToken { * PureSuperToken doesn't have any underlying ERC20 token. */ export class PureSuperToken extends SuperToken { - constructor(options: ITokenOptions, settings: ITokenSettings) { + override readonly constantOutflowNFTProxy: ConstantOutflowNFT; + override readonly constantInflowNFTProxy: ConstantInflowNFT; + + constructor( + options: ITokenOptions, + settings: ITokenSettings, + nftAddresses: NFTAddresses + ) { super(options, settings); + this.constantInflowNFTProxy = new ConstantInflowNFT( + nftAddresses.constantInflowNFTProxy + ); + this.constantOutflowNFTProxy = new ConstantOutflowNFT( + nftAddresses.constantOutflowNFTProxy + ); } } @@ -864,13 +911,23 @@ export class PureSuperToken extends SuperToken { */ export class NativeAssetSuperToken extends SuperToken { readonly nativeTokenSymbol: string; + override readonly constantOutflowNFTProxy: ConstantOutflowNFT; + override readonly constantInflowNFTProxy: ConstantInflowNFT; + constructor( options: ITokenOptions, settings: ITokenSettings, - nativeTokenSymbol: string + nativeTokenSymbol: string, + nftAddresses: NFTAddresses ) { super(options, settings); this.nativeTokenSymbol = nativeTokenSymbol; + this.constantInflowNFTProxy = new ConstantInflowNFT( + nftAddresses.constantInflowNFTProxy + ); + this.constantOutflowNFTProxy = new ConstantOutflowNFT( + nftAddresses.constantOutflowNFTProxy + ); } get nativeAssetContract() { diff --git a/packages/sdk-core/src/interfaces.ts b/packages/sdk-core/src/interfaces.ts index 7d337656f5..b2e532ee50 100644 --- a/packages/sdk-core/src/interfaces.ts +++ b/packages/sdk-core/src/interfaces.ts @@ -12,6 +12,8 @@ import { // Maybe moving these into categorical files // makes more sense than stuffing them all here +export type ProviderOrSigner = ethers.providers.Provider | ethers.Signer; + // read request interfaces export interface IAccountTokenSnapshotFilter { readonly account?: string; @@ -87,16 +89,16 @@ export interface ISuperTokenGetSubscriptionParams { readonly indexId: string; readonly publisher: string; readonly subscriber: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface ISuperTokenGetIndexParams { readonly indexId: string; readonly publisher: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface ISuperTokenPublisherParams extends ISuperTokenBaseIDAParams { readonly publisher: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface ISuperTokenPubSubParams extends EthersParams { readonly indexId: string; @@ -177,7 +179,7 @@ export interface IFullControlParams } export interface IRealtimeBalanceOfParams { - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; readonly account: string; readonly timestamp?: number; } @@ -202,53 +204,53 @@ export interface ERC777SendParams extends EthersParams { export interface ISuperTokenGetFlowParams { readonly sender: string; readonly receiver: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface ISuperTokenGetFlowInfoParams { readonly account: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IGetFlowParams { readonly superToken: string; readonly sender: string; readonly receiver: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IGetAccountFlowInfoParams { readonly superToken: string; readonly account: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IGetFlowOperatorDataParams { readonly superToken: string; readonly sender: string; readonly flowOperator: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IGetFlowOperatorDataByIDParams { readonly superToken: string; readonly flowOperatorId: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IGetGovernanceParametersParams { - providerOrSigner: ethers.providers.Provider | ethers.Signer; + providerOrSigner: ProviderOrSigner; token?: string; } export interface ISuperTokenFlowOperatorDataParams { readonly sender: string; readonly flowOperator: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface ISuperTokenFlowOperatorDataByIDParams { readonly flowOperatorId: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IBaseIDAParams { @@ -270,11 +272,11 @@ export interface IBaseSubscriptionParams extends IBaseIDAParams { export interface IGetSubscriptionParams extends IBaseIDAParams { readonly publisher: string; readonly subscriber: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IGetIndexParams extends IBaseIDAParams { readonly publisher: string; - readonly providerOrSigner: ethers.providers.Provider | ethers.Signer; + readonly providerOrSigner: ProviderOrSigner; } export interface IDistributeParams extends EthersParams { @@ -512,6 +514,66 @@ export interface IWeb3GovernanceParams { readonly minimumDeposit: string; } +export interface ERC20BalanceOfParams { + readonly account: string; + readonly providerOrSigner: ProviderOrSigner; +} +export interface ERC20AllowanceParams { + readonly owner: string; + readonly spender: string; + readonly providerOrSigner: ProviderOrSigner; +} +export interface ERC20BalanceOfParams { + readonly account: string; + readonly providerOrSigner: ProviderOrSigner; +} + +// ERC721 + +export interface NFTFlowData { + readonly flowSender: string; + readonly flowStartDate: Date; + readonly flowReceiver: string; +} + +export interface ERC721TransferFromParams extends EthersParams { + readonly from: string; + readonly to: string; + readonly tokenId: string; +} + +export interface ERC721SafeTransferFromParams extends ERC721TransferFromParams { + readonly data: string; +} + +export interface ERC721ApproveParams extends EthersParams { + readonly approved: string; + readonly tokenId: string; +} + +export interface ERC721SetApprovalForAllParams extends EthersParams { + readonly operator: string; + readonly approved: boolean; +} + +export interface ERC721BalanceOfParams { + readonly owner: string; + readonly providerOrSigner: ProviderOrSigner; +} + +export interface ERC721TokenIdQueryParams { + readonly tokenId: string; + readonly providerOrSigner: ProviderOrSigner; +} +export interface ERC721IsApprovedForAllParams { + readonly owner: string; + readonly operator: string; + readonly providerOrSigner: ProviderOrSigner; +} + +export type ERC721OwnerOfParams = ERC721TokenIdQueryParams; +export type ERC721GetApprovedParams = ERC721TokenIdQueryParams; +export type ERC721TokenURIParams = ERC721TokenIdQueryParams; export interface ERC20IncreaseAllowanceParams extends EthersParams { readonly spender: string; readonly amount: string; diff --git a/packages/sdk-core/test/1.3_supertoken_ida.test.ts b/packages/sdk-core/test/1.3_supertoken_ida.test.ts index aa77931f90..2c2e3306ad 100644 --- a/packages/sdk-core/test/1.3_supertoken_ida.test.ts +++ b/packages/sdk-core/test/1.3_supertoken_ida.test.ts @@ -3,7 +3,6 @@ import { ethers } from "ethers"; import { makeSuite, TestEnvironment } from "./TestEnvironment"; makeSuite("SuperToken-IDA Tests", (testEnv: TestEnvironment) => { - // Note: Alpha will create the Index which Deployer and Bravo describe("Revert cases", () => { it("Should throw an error if one of the input addresses is invalid", async () => { try { diff --git a/packages/sdk-core/test/1.4_supertoken_nft.test.ts b/packages/sdk-core/test/1.4_supertoken_nft.test.ts new file mode 100644 index 0000000000..8ff3085f3a --- /dev/null +++ b/packages/sdk-core/test/1.4_supertoken_nft.test.ts @@ -0,0 +1,348 @@ +import { expect } from "chai"; + +import { makeSuite, TestEnvironment } from "./TestEnvironment"; +import { getPerSecondFlowRateByMonth } from "../src"; + +const createFlow = async (testEnv: TestEnvironment) => { + const flowRate = getPerSecondFlowRateByMonth("1000"); + await testEnv.wrapperSuperToken + .createFlow({ + sender: testEnv.alice.address, + receiver: testEnv.bob.address, + flowRate, + }) + .exec(testEnv.alice); + + return await testEnv.wrapperSuperToken.constantOutflowNFTProxy.getTokenId({ + superToken: testEnv.wrapperSuperToken.address, + sender: testEnv.alice.address, + receiver: testEnv.bob.address, + providerOrSigner: testEnv.alice, + }); +}; + +makeSuite("SuperToken-NFT Tests", (testEnv: TestEnvironment) => { + describe("Revert cases", () => { + it("Should revert when trying to transferFrom", async () => { + const tokenId = await createFlow(testEnv); + + await expect( + testEnv.wrapperSuperToken.constantOutflowNFTProxy + .transferFrom({ + from: testEnv.alice.address, + to: testEnv.bob.address, + tokenId, + }) + .exec(testEnv.alice) + ).to.be.revertedWithCustomError( + testEnv.wrapperSuperToken.constantOutflowNFTProxy.contract, + "CFA_NFT_TRANSFER_IS_NOT_ALLOWED" + ); + }); + + it("Should revert when trying to safeTransferFrom", async () => { + const tokenId = await createFlow(testEnv); + + await expect( + testEnv.wrapperSuperToken.constantOutflowNFTProxy + .safeTransferFrom({ + from: testEnv.alice.address, + to: testEnv.bob.address, + tokenId, + }) + .exec(testEnv.alice) + ).to.be.revertedWithCustomError( + testEnv.wrapperSuperToken.constantOutflowNFTProxy.contract, + "CFA_NFT_TRANSFER_IS_NOT_ALLOWED" + ); + }); + + it("Should revert when trying to safeTransferFromWithData", async () => { + const tokenId = await createFlow(testEnv); + + await expect( + testEnv.wrapperSuperToken.constantOutflowNFTProxy + .safeTransferFromWithData({ + from: testEnv.alice.address, + to: testEnv.bob.address, + tokenId, + data: "0x", + }) + .exec(testEnv.alice) + ).to.be.revertedWithCustomError( + testEnv.wrapperSuperToken.constantOutflowNFTProxy.contract, + "CFA_NFT_TRANSFER_IS_NOT_ALLOWED" + ); + }); + + it("Should revert if ownerOf token does not exist", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.ownerOf( + { + tokenId: "69", + providerOrSigner: testEnv.alice, + } + ); + } catch (err: any) { + expect(err.message).to.contain("CFA_NFT_INVALID_TOKEN_ID"); + } + }); + + it("Should revert if approve to owner", async () => { + const tokenId = await createFlow(testEnv); + + await expect( + testEnv.wrapperSuperToken.constantOutflowNFTProxy + .approve({ + approved: testEnv.alice.address, + tokenId, + }) + .exec(testEnv.alice) + ).to.be.revertedWithCustomError( + testEnv.wrapperSuperToken.constantOutflowNFTProxy.contract, + "CFA_NFT_APPROVE_TO_CURRENT_OWNER" + ); + }); + + it("Should revert if approve on behalf of someone else", async () => { + const tokenId = await createFlow(testEnv); + + await expect( + testEnv.wrapperSuperToken.constantOutflowNFTProxy + .approve({ + approved: testEnv.bob.address, + tokenId, + }) + .exec(testEnv.bob) + ).to.be.revertedWithCustomError( + testEnv.wrapperSuperToken.constantOutflowNFTProxy.contract, + "CFA_NFT_APPROVE_CALLER_NOT_OWNER_OR_APPROVED_FOR_ALL" + ); + }); + + it("Should catch error in balanceOf", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.balanceOf( + { + owner: "0x", + providerOrSigner: testEnv.alice, + } + ); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting balanceOf" + ); + } + }); + + it("Should catch error in getApproved", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.getApproved( + { + tokenId: "0x", + providerOrSigner: testEnv.alice, + } + ); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting getApproved" + ); + } + }); + + it("Should catch error in isApprovedForAll", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.isApprovedForAll( + { + owner: "0x", + operator: "0x", + providerOrSigner: testEnv.alice, + } + ); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting isApprovedForAll" + ); + } + }); + + it("Should catch error in name", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.name({ + providerOrSigner: testEnv.alice, + }); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting name" + ); + } + }); + + it("Should catch error in symbol", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.symbol({ + providerOrSigner: testEnv.alice, + }); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting symbol" + ); + } + }); + + it("Should catch error in tokenURI", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.tokenURI( + { + tokenId: "0x", + providerOrSigner: testEnv.alice, + } + ); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting tokenURI" + ); + } + }); + + it("Should catch error in getTokenId", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.getTokenId( + { + superToken: testEnv.wrapperSuperToken.address, + sender: testEnv.alice.address, + receiver: testEnv.bob.address, + providerOrSigner: "testEnv.alice" as any, + } + ); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting token id" + ); + } + }); + + it("Should catch error in flowDataByTokenId", async () => { + try { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.flowDataByTokenId( + { + tokenId: "0x", + providerOrSigner: testEnv.alice, + } + ); + } catch (err: any) { + expect(err.message).to.contain( + "There was an error getting flow data by token id" + ); + } + }); + }); + + describe("Happy Path Tests", () => { + it("Should be able to get flowDataByTokenId", async () => { + const tokenId = await createFlow(testEnv); + + const flowData = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.flowDataByTokenId( + { + tokenId, + providerOrSigner: testEnv.alice, + } + ); + expect(flowData.flowSender).to.equal(testEnv.alice.address); + expect(flowData.flowReceiver).to.equal(testEnv.bob.address); + }); + + it("Should be able to approve", async () => { + const tokenId = await createFlow(testEnv); + + await testEnv.wrapperSuperToken.constantOutflowNFTProxy + .approve({ + approved: testEnv.bob.address, + tokenId, + }) + .exec(testEnv.alice); + + const approved = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.getApproved( + { + tokenId, + providerOrSigner: testEnv.alice, + } + ); + expect(approved).to.equal(testEnv.bob.address); + }); + + it("Should be able to setApprovalForAll", async () => { + await testEnv.wrapperSuperToken.constantOutflowNFTProxy + .setApprovalForAll({ + operator: testEnv.bob.address, + approved: true, + }) + .exec(testEnv.alice); + + const approved = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.isApprovedForAll( + { + owner: testEnv.alice.address, + operator: testEnv.bob.address, + providerOrSigner: testEnv.alice, + } + ); + expect(approved).to.equal(true); + }); + + it("Should be able to get ownerOf", async () => { + const tokenId = await createFlow(testEnv); + + const owner = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.ownerOf( + { + tokenId, + providerOrSigner: testEnv.alice, + } + ); + expect(owner).to.equal(testEnv.alice.address); + }); + + it("Should be able to get balanceOf (always returns 1)", async () => { + const balance = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.balanceOf( + { + owner: testEnv.alice.address, + providerOrSigner: testEnv.alice, + } + ); + expect(balance.toString()).to.equal("1"); + }); + + it("Should be able to get name", async () => { + const name = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.name({ + providerOrSigner: testEnv.alice, + }); + expect(name).to.equal("Constant Outflow NFT"); + }); + + it("Should be able to get tokenURI", async () => { + const tokenId = await createFlow(testEnv); + + const tokenURI = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.tokenURI( + { + tokenId, + providerOrSigner: testEnv.alice, + } + ); + expect(tokenURI).to.not.be.empty; + }); + + it("Should be able to get symbol", async () => { + const symbol = + await testEnv.wrapperSuperToken.constantOutflowNFTProxy.symbol({ + providerOrSigner: testEnv.alice, + }); + expect(symbol.toString()).to.equal("COF"); + }); + }); +}); diff --git a/packages/subgraph/config/mock.json b/packages/subgraph/config/mock.json index 430c2ab050..2828806c15 100644 --- a/packages/subgraph/config/mock.json +++ b/packages/subgraph/config/mock.json @@ -6,5 +6,7 @@ "idaAddress": "0x0000000000000000000000000000000000000000", "superTokenFactoryAddress": "0x0000000000000000000000000000000000000000", "resolverV1Address": "0x0000000000000000000000000000000000000000", - "nativeAssetSuperTokenAddress": "0x0000000000000000000000000000000000000000" + "nativeAssetSuperTokenAddress": "0x0000000000000000000000000000000000000000", + "constantOutflowNFTAddress": "0x0000000000000000000000000000000000000000", + "constantInflowNFTAddress": "0x0000000000000000000000000000000000000000" } diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index a122e43fc7..3082a4806f 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -482,7 +482,8 @@ type SubscriptionApprovedEvent implements Event @entity(immutable: true) { subscription: IndexSubscription! } -type SubscriptionDistributionClaimedEvent implements Event @entity(immutable: true) { +type SubscriptionDistributionClaimedEvent implements Event + @entity(immutable: true) { id: ID! transactionHash: Bytes! gasPrice: BigInt! @@ -835,7 +836,8 @@ type SetEvent implements Event @entity(immutable: true) { } # SuperfluidGovernance # -type CFAv1LiquidationPeriodChangedEvent implements Event @entity(immutable: true) { +type CFAv1LiquidationPeriodChangedEvent implements Event + @entity(immutable: true) { id: ID! transactionHash: Bytes! gasPrice: BigInt! @@ -1179,7 +1181,7 @@ type TransferEvent implements Event @entity(immutable: true) { """ Contains the addresses that were impacted by this event: - addresses[0] = `token` (superToken) + addresses[0] = `token` (superToken if `isNFTTransfer` is false, otherwise the ConstantOutflowNFT or ConstantInflowNFT) addresses[1] = `from` addresses[2] = `to` """ @@ -1190,7 +1192,15 @@ type TransferEvent implements Event @entity(immutable: true) { from: Account! to: Account! + isNFTTransfer: Boolean! + """ + If `isNFTTransfer` is true, value is the `tokenId` of the NFT transferred. + """ value: BigInt! + + """ + If `isNFTTransfer` is true, value is the NFT address, else it is the SuperToken address. + """ token: Bytes! } @@ -1240,6 +1250,88 @@ type TokenUpgradedEvent implements Event @entity(immutable: true) { amount: BigInt! } + +# NFTs # +type ApprovalEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Empty addresses array. + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + owner: Account! + + """ + The address that will be granted allowance to transfer the NFT. + """ + to: Account! + """ + The id of the NFT that will be granted allowance to transfer. + The id is: uint256(keccak256(abi.encode(block.chainid, superToken, sender, receiver))) + """ + tokenId: BigInt! +} + +type ApprovalForAllEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Empty addresses array. + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + owner: Account! + + """ + The address that will be granted operator permissions for the all of the owner's tokens. + """ + operator: Account! + """ + Whether the operator is enabled or disabled for `owner`. + """ + approved: Boolean! +} + +type MetadataUpdateEvent implements Event @entity(immutable: true) { + id: ID! + transactionHash: Bytes! + gasPrice: BigInt! + gasUsed: BigInt! + timestamp: BigInt! + name: String! + + """ + Empty addresses array. + """ + addresses: [Bytes!]! + blockNumber: BigInt! + logIndex: BigInt! + order: BigInt! + + """ + The id of the NFT that will be granted allowance to transfer. + The id is: uint256(keccak256(abi.encode(block.chainid, superToken, sender, receiver))) + """ + tokenId: BigInt! +} + # SuperTokenFactory # type CustomSuperTokenCreatedEvent implements Event @entity(immutable: true) { diff --git a/packages/subgraph/scripts/buildNetworkConfig.ts b/packages/subgraph/scripts/buildNetworkConfig.ts index 35a073eba6..39bbb12a9f 100644 --- a/packages/subgraph/scripts/buildNetworkConfig.ts +++ b/packages/subgraph/scripts/buildNetworkConfig.ts @@ -10,8 +10,12 @@ interface SubgraphConfig { readonly superTokenFactoryAddress: string; readonly resolverV1Address: string; readonly nativeAssetSuperTokenAddress: string; + readonly constantOutflowNFTAddress: string; + readonly constantInflowNFTAddress: string; } +const ADDRESS_ZERO = "0x0000000000000000000000000000000000000000"; + // script usage: npx ts-node ./scripts/buildNetworkConfig.ts function main() { const networkName = process.argv[2]; @@ -21,7 +25,7 @@ function main() { if (!networkMetadata) { throw new Error("No metadata found"); } - const newThing: SubgraphConfig = { + const subgraphConfig: SubgraphConfig = { network: networkMetadata.shortName, hostStartBlock: networkMetadata.startBlockV1, hostAddress: networkMetadata.contractsV1.host, @@ -30,12 +34,16 @@ function main() { superTokenFactoryAddress: networkMetadata.contractsV1.superTokenFactory, resolverV1Address: networkMetadata.contractsV1.resolver, nativeAssetSuperTokenAddress: networkMetadata.nativeTokenWrapper, + constantOutflowNFTAddress: + networkMetadata.contractsV1.constantOutflowNFT || ADDRESS_ZERO, + constantInflowNFTAddress: + networkMetadata.contractsV1.constantInflowNFT || ADDRESS_ZERO, }; const writeToDir = __dirname.split("subgraph")[0] + `subgraph/config/${networkName}.json`; - fs.writeFile(writeToDir, JSON.stringify(newThing), (err) => { + fs.writeFile(writeToDir, JSON.stringify(subgraphConfig), (err) => { if (err) { console.log(err); process.exit(1); diff --git a/packages/subgraph/scripts/getAbi.js b/packages/subgraph/scripts/getAbi.js index 925b98df04..60a3c65834 100755 --- a/packages/subgraph/scripts/getAbi.js +++ b/packages/subgraph/scripts/getAbi.js @@ -5,6 +5,7 @@ const contracts = [ "ConstantFlowAgreementV1", "ERC20", "IConstantFlowAgreementV1", + "IFlowNFTBase", "IResolver", "ISuperTokenFactory", "ISuperToken", diff --git a/packages/subgraph/src/mappings/flowNFT.ts b/packages/subgraph/src/mappings/flowNFT.ts new file mode 100644 index 0000000000..2efd3215d8 --- /dev/null +++ b/packages/subgraph/src/mappings/flowNFT.ts @@ -0,0 +1,60 @@ +import { + Approval, + ApprovalForAll, + Transfer, + MetadataUpdate, +} from "../../generated/ConstantInflowNFT/IFlowNFTBase"; +import { + ApprovalEvent, + ApprovalForAllEvent, + MetadataUpdateEvent, + TransferEvent, +} from "../../generated/schema"; +import { createEventID, initializeEventEntity } from "../utils"; + +export function handleApproval(event: Approval): void { + const eventId = createEventID("Approval", event); + const ev = new ApprovalEvent(eventId); + ev.owner = event.params.owner.toHex(); + ev.to = event.params.approved.toHex(); + ev.tokenId = event.params.tokenId; + + ev.save(); +} + +export function handleApprovalForAll(event: ApprovalForAll): void { + const eventId = createEventID("ApprovalForAll", event); + const ev = new ApprovalForAllEvent(eventId); + initializeEventEntity(ev, event, []); + ev.owner = event.params.owner.toHex(); + ev.operator = event.params.operator.toHex(); + ev.approved = event.params.approved; + + ev.save(); +} + +export function handleTransfer(event: Transfer): void { + const eventId = createEventID("Transfer", event); + const ev = new TransferEvent(eventId); + initializeEventEntity(ev, event, [ + event.address, + event.params.from, + event.params.to, + ]); + ev.isNFTTransfer = true; + ev.from = event.params.from.toHex(); + ev.to = event.params.to.toHex(); + ev.value = event.params.tokenId; + ev.token = event.address; + + ev.save(); +} + +export function handleMetadataUpdate(event: MetadataUpdate): void { + const eventId = createEventID("MetadataUpdate", event); + const ev = new MetadataUpdateEvent(eventId); + initializeEventEntity(ev, event, []); + ev.tokenId = event.params.tokenId; + + ev.save(); +} diff --git a/packages/subgraph/src/mappings/superToken.ts b/packages/subgraph/src/mappings/superToken.ts index 432b72399d..a6e10d496d 100644 --- a/packages/subgraph/src/mappings/superToken.ts +++ b/packages/subgraph/src/mappings/superToken.ts @@ -443,6 +443,7 @@ function _createTransferEventEntity(event: Transfer): void { event.params.from, event.params.to, ]); + ev.isNFTTransfer = false; ev.from = event.params.from.toHex(); ev.to = event.params.to.toHex(); ev.value = event.params.value; diff --git a/packages/subgraph/subgraph.template.yaml b/packages/subgraph/subgraph.template.yaml index 1b7356ee5b..4ce9664562 100644 --- a/packages/subgraph/subgraph.template.yaml +++ b/packages/subgraph/subgraph.template.yaml @@ -232,6 +232,72 @@ dataSources: - event: Set(indexed string,address) handler: handleSet receipt: true + - kind: ethereum/contract + name: ConstantOutflowNFT + network: {{ network }} + source: + address: "{{ constantOutflowNFTAddress }}" + abi: IFlowNFTBase + startBlock: {{ hostStartBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + file: ./src/mappings/flowNFT.ts + entities: + - ApprovalEvent + - ApprovalForAllEvent + - MetadataUpdateEvent + - TransferEvent + abis: + - name: IFlowNFTBase + file: ./abis/IFlowNFTBase.json + eventHandlers: + - event: Transfer(indexed address,indexed address,indexed uint256) + handler: handleTransfer + receipt: true + - event: Approval(indexed address,indexed address,indexed uint256) + handler: handleApproval + receipt: true + - event: ApprovalForAll(indexed address,indexed address,bool) + handler: handleApprovalForAll + receipt: true + - event: MetadataUpdate(uint256) + handler: handleMetadataUpdate + receipt: true + - kind: ethereum/contract + name: ConstantInflowNFT + network: {{ network }} + source: + address: "{{ constantInflowNFTAddress }}" + abi: IFlowNFTBase + startBlock: {{ hostStartBlock }} + mapping: + kind: ethereum/events + apiVersion: 0.0.7 + language: wasm/assemblyscript + file: ./src/mappings/flowNFT.ts + entities: + - ApprovalEvent + - ApprovalForAllEvent + - MetadataUpdateEvent + - TransferEvent + abis: + - name: IFlowNFTBase + file: ./abis/IFlowNFTBase.json + eventHandlers: + - event: Transfer(indexed address,indexed address,indexed uint256) + handler: handleTransfer + receipt: true + - event: Approval(indexed address,indexed address,indexed uint256) + handler: handleApproval + receipt: true + - event: ApprovalForAll(indexed address,indexed address,bool) + handler: handleApprovalForAll + receipt: true + - event: MetadataUpdate(uint256) + handler: handleMetadataUpdate + receipt: true templates: - name: SuperToken kind: ethereum/contract @@ -292,6 +358,12 @@ templates: - event: Approval(indexed address,indexed address,uint256) handler: handleApproval receipt: true + - event: ConstantOutflowNFTCreated(indexed address) + handler: handleConstantOutflowNFTCreated + receipt: true + - event: ConstantInflowNFTCreated(indexed address) + handler: handleConstantInflowNFTCreated + receipt: true - kind: ethereum/contract name: SuperfluidGovernance network: {{ network }}