-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #34 from pbkompasz/ingestor-coinbase-wallet
Ingestor wallet.coinbase
- Loading branch information
Showing
7 changed files
with
361 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
export const MINT_CONTRACT_ABI = [ | ||
{ | ||
inputs: [ | ||
{ internalType: 'address', name: 'to', type: 'address' }, | ||
{ internalType: 'uint256', name: 'quantity', type: 'uint256' }, | ||
{ internalType: 'string', name: 'comment', type: 'string' }, | ||
], | ||
name: 'mintWithComment', | ||
outputs: [], | ||
stateMutability: 'payable', | ||
type: 'function', | ||
}, | ||
{ | ||
inputs: [], | ||
name: 'metadata', | ||
outputs: [ | ||
{ internalType: 'address', name: 'creator', type: 'address' }, | ||
{ internalType: 'string', name: 'name', type: 'string' }, | ||
{ internalType: 'string', name: 'description', type: 'string' }, | ||
{ internalType: 'string', name: 'symbol', type: 'string' }, | ||
{ internalType: 'string', name: 'image', type: 'string' }, | ||
{ internalType: 'string', name: 'animation_url', type: 'string' }, | ||
{ internalType: 'string', name: 'mintType', type: 'string' }, | ||
{ internalType: 'uint128', name: 'maxSupply', type: 'uint128' }, | ||
{ internalType: 'uint128', name: 'maxPerWallet', type: 'uint128' }, | ||
{ internalType: 'uint256', name: 'cost', type: 'uint256' }, | ||
{ internalType: 'uint256', name: 'startTime', type: 'uint256' }, | ||
{ internalType: 'uint256', name: 'endTime', type: 'uint256' }, | ||
{ internalType: 'uint256', name: 'nonce', type: 'uint256' }, | ||
], | ||
stateMutability: 'view', | ||
type: 'function', | ||
}, | ||
{ | ||
inputs: [{ internalType: 'uint256', name: 'quantity', type: 'uint256' }], | ||
name: 'cost', | ||
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], | ||
stateMutability: 'view', | ||
type: 'function', | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import { MintContractOptions, MintIngestor, MintIngestorResources } from '../../lib/types/mint-ingestor'; | ||
import { MintIngestionErrorName, MintIngestorError } from '../../lib/types/mint-ingestor-error'; | ||
import { MintInstructionType, MintTemplate } from '../../lib/types/mint-template'; | ||
import { MintTemplateBuilder } from '../../lib/builder/mint-template-builder'; | ||
import { getCoinbaseWalletCreator } from './offchain-metadata'; | ||
import { MINT_CONTRACT_ABI } from './abi'; | ||
import { getCoinbaseWalletMetadata, getCoinbaseWalletPriceInWei } from './onchain-metadata'; | ||
|
||
export class CoinbaseWalletIngestor implements MintIngestor { | ||
async supportsUrl(resources: MintIngestorResources, url: string): Promise<boolean> { | ||
const collectionDescriptor = url.split('/').pop(); | ||
if (!collectionDescriptor) { | ||
return false; | ||
} | ||
const collectionDescriptorSplit = collectionDescriptor.split(':'); | ||
if (collectionDescriptorSplit.length !== 4) return false; | ||
if (collectionDescriptorSplit[1] !== '8453') return false; | ||
|
||
return new URL(url).hostname === 'www.wallet.coinbase.com' || new URL(url).hostname === 'wallet.coinbase.com'; | ||
} | ||
|
||
async supportsContract(resources: MintIngestorResources, contractOptions: MintContractOptions): Promise<boolean> { | ||
if (contractOptions.chainId !== 8453) { | ||
return false; | ||
} | ||
const collection = await getCoinbaseWalletMetadata(8453, contractOptions.contractAddress, resources.alchemy); | ||
if (!collection) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
async createMintForContract( | ||
resources: MintIngestorResources, | ||
contractOptions: MintContractOptions, | ||
): Promise<MintTemplate> { | ||
const mintBuilder = new MintTemplateBuilder() | ||
.setMintInstructionType(MintInstructionType.EVM_MINT) | ||
.setPartnerName('CoinbaseWallet'); | ||
|
||
if (contractOptions.url) { | ||
mintBuilder.setMarketingUrl(contractOptions.url); | ||
} | ||
|
||
const collectionMetadata = await getCoinbaseWalletMetadata( | ||
8453, | ||
contractOptions.contractAddress as string, | ||
resources.alchemy, | ||
); | ||
|
||
if (!collectionMetadata) { | ||
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'No such collection'); | ||
} | ||
|
||
const contractAddress = contractOptions.contractAddress; | ||
const description = collectionMetadata.description; | ||
const formattedImage = `https://ipfs.io/ipfs/${collectionMetadata.image.split('//').pop()}`; | ||
|
||
mintBuilder.setName(collectionMetadata.name).setDescription(description).setFeaturedImageUrl(formattedImage); | ||
mintBuilder.setMintOutputContract({ chainId: 8453, address: contractAddress }); | ||
|
||
// Some collections creators do not have valid metadata, only address | ||
const creator = await getCoinbaseWalletCreator(resources, collectionMetadata.creator); | ||
|
||
mintBuilder.setCreator({ | ||
name: creator?.name ?? '', | ||
walletAddress: collectionMetadata.creator, | ||
imageUrl: creator?.avatar, | ||
}); | ||
|
||
const totalPriceWei = await getCoinbaseWalletPriceInWei(8453, contractAddress, resources.alchemy); | ||
|
||
if (!totalPriceWei) { | ||
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Price not available'); | ||
} | ||
|
||
mintBuilder.setMintInstructions({ | ||
chainId: 8453, | ||
contractAddress, | ||
contractMethod: 'mintWithComment', | ||
contractParams: `[address, 1, ""]`, | ||
abi: MINT_CONTRACT_ABI, | ||
priceWei: totalPriceWei, | ||
}); | ||
|
||
const liveDate = | ||
+new Date() > collectionMetadata.startTime * 1000 ? new Date() : new Date(collectionMetadata.startTime * 1000); | ||
mintBuilder | ||
.setAvailableForPurchaseStart(new Date(+collectionMetadata.startTime * 1000 || Date.now())) | ||
.setAvailableForPurchaseEnd(new Date(+collectionMetadata.endTime * 1000 || '2030-01-01')) | ||
.setLiveDate(liveDate); | ||
|
||
return mintBuilder.build(); | ||
} | ||
|
||
async createMintTemplateForUrl(resources: MintIngestorResources, url: string): Promise<MintTemplate> { | ||
const isCompatible = await this.supportsUrl(resources, url); | ||
if (!isCompatible) { | ||
throw new MintIngestorError(MintIngestionErrorName.IncompatibleUrl, 'Incompatible URL'); | ||
} | ||
|
||
const collectionDescriptor = url.split('/').pop(); | ||
if (!collectionDescriptor) { | ||
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Url error'); | ||
} | ||
|
||
const collectionDescriptorSplit = collectionDescriptor.split(':'); | ||
const contractAddress = collectionDescriptorSplit.pop() as string; | ||
collectionDescriptorSplit.pop(); | ||
const chainId = collectionDescriptorSplit.pop() as string; | ||
|
||
const collectionMetadata = await getCoinbaseWalletMetadata(+chainId, contractAddress as string, resources.alchemy); | ||
|
||
if (!collectionMetadata) { | ||
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'No such collection'); | ||
} | ||
|
||
return this.createMintForContract(resources, { | ||
chainId: 8453, | ||
contractAddress, | ||
url, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { MintIngestorResources } from 'src/lib'; | ||
import { Profile } from './types'; | ||
|
||
export const getCoinbaseWalletCreator = async (resources: MintIngestorResources, address: string): Promise<Profile | undefined> => { | ||
const url = `https://api.wallet.coinbase.com/rpc/v2/getBasicPublicProfiles?addresses=${address}`; | ||
|
||
try { | ||
const resp = await resources.fetcher.get(url); | ||
const profile = resp.data.result.profiles[address]; | ||
return profile; | ||
} catch (error) {} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { Alchemy, Contract } from 'alchemy-sdk'; | ||
import { MINT_CONTRACT_ABI } from './abi'; | ||
import { CollectionMetadata } from './types'; | ||
|
||
const getContract = async (chainId: number, contractAddress: string, alchemy: Alchemy): Promise<Contract> => { | ||
const ethersProvider = await alchemy.config.getProvider(); | ||
const contract = new Contract(contractAddress, MINT_CONTRACT_ABI, ethersProvider); | ||
return contract; | ||
}; | ||
|
||
export const getCoinbaseWalletMetadata = async ( | ||
chainId: number, | ||
contractAddress: string, | ||
alchemy: Alchemy, | ||
): Promise<CollectionMetadata | undefined> => { | ||
try { | ||
const contract = await getContract(chainId, contractAddress, alchemy); | ||
const metadata = await contract.functions.metadata(); | ||
|
||
return { | ||
...metadata, | ||
cost: parseInt(String(metadata.cost)), | ||
startTime: parseInt(String(metadata.startTime)), | ||
endTime: parseInt(String(metadata.endTime)) | ||
}; | ||
} catch (error) { | ||
console.log(error) | ||
} | ||
}; | ||
|
||
export const getCoinbaseWalletPriceInWei = async ( | ||
chainId: number, | ||
contractAddress: string, | ||
alchemy: Alchemy, | ||
): Promise<string | undefined> => { | ||
try { | ||
const contract = await getContract(chainId, contractAddress, alchemy); | ||
const price = await contract.functions.cost(1); | ||
|
||
return `${parseInt(String(price))}`; | ||
|
||
} catch (error) { | ||
console.log(error) | ||
} | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export type CollectionMetadata = { | ||
creator: string, | ||
name: string, | ||
description: string, | ||
image: string, | ||
mintType: 'OPEN_EDITION_721', | ||
cost: number, | ||
startTime: number, | ||
endTime: number, | ||
} | ||
|
||
export type Profile = { | ||
address: string; | ||
name: string; | ||
avatar: string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { expect } from 'chai'; | ||
import { CoinbaseWalletIngestor } from '../../src/ingestors/coinbase-wallet'; | ||
import { mintIngestorResources } from '../../src/lib/resources'; | ||
import { EVMMintInstructions } from '../../src/lib/types/mint-template'; | ||
import { MintTemplateBuilder } from '../../src/lib/builder/mint-template-builder'; | ||
import { basicIngestorTests } from '../shared/basic-ingestor-tests'; | ||
|
||
const resources = mintIngestorResources(); | ||
|
||
describe('CoinbaseWallet', function () { | ||
basicIngestorTests( | ||
new CoinbaseWalletIngestor(), | ||
resources, | ||
{ | ||
successUrls: [ | ||
'https://wallet.coinbase.com/nft/mint/eip155:8453:erc721:0xb5408b7126142C61f509046868B1273F96191b6d', | ||
'https://wallet.coinbase.com/nft/mint/eip155:8453:erc721:0xEE8128c612ABE57070dEac0E299282ef0a71a347', | ||
'https://wallet.coinbase.com/nft/mint/eip155:8453:erc721:0x3aDa53708b167Fbd2907b5a1bA19b58a856E2200' | ||
], | ||
failureUrls: [ | ||
'https://wallet.coinbase.com/nft/gallery/ethereum-etf', | ||
'https://foundation.app/mint/base/the-billows', | ||
], | ||
successContracts: [ | ||
{ chainId: 8453, contractAddress: '0x13F294BF5e26843C33d0ae739eDb8d6B178740B0' }, | ||
{ chainId: 8453, contractAddress: '0xEE8128c612ABE57070dEac0E299282ef0a71a347' }, | ||
], | ||
failureContracts: [{ chainId: 5000, contractAddress: '0x62F8C536De24ED32611f128f64F6eAbd9b82176c' }], | ||
}, | ||
{ | ||
'8453': '0x1175BA0', | ||
}, | ||
); | ||
it('supportsUrl: Returns false for an unsupported URL', async function () { | ||
const ingestor = new CoinbaseWalletIngestor(); | ||
const url = 'https://wallet.coinbase.com/nft/gallery/ethereum-etf'; | ||
const resources = mintIngestorResources(); | ||
const result = await ingestor.supportsUrl(resources, url); | ||
expect(result).to.be.false; | ||
}); | ||
|
||
it('supportsUrl: Returns true for a supported URL', async function () { | ||
const ingestor = new CoinbaseWalletIngestor(); | ||
const url = 'https://wallet.coinbase.com/nft/mint/eip155:8453:erc721:0xb5408b7126142C61f509046868B1273F96191b6d'; | ||
const resources = mintIngestorResources(); | ||
const result = await ingestor.supportsUrl(resources, url); | ||
expect(result).to.be.true; | ||
|
||
const url2 = 'https://wallet.coinbase.com/nft/mint/eip155:8453:erc721:0x17ca8424F3c42aA840eB1B54a2400eC80330Eea8'; | ||
const result2 = await ingestor.supportsUrl(resources, url2); | ||
expect(result2).to.be.true; | ||
}); | ||
|
||
it('createMintTemplateForUrl: Returns a mint template for a supported URL', async function () { | ||
const ingestor = new CoinbaseWalletIngestor(); | ||
const url = 'https://wallet.coinbase.com/nft/mint/eip155:8453:erc721:0x17ca8424F3c42aA840eB1B54a2400eC80330Eea8'; | ||
const resources = mintIngestorResources(); | ||
const template = await ingestor.createMintTemplateForUrl(resources, url); | ||
|
||
// Verify that the mint template passed validation | ||
const builder = new MintTemplateBuilder(template); | ||
builder.validateMintTemplate(); | ||
|
||
expect(template.name).to.equal('Italy Is Based'); | ||
expect(template.description).to.contain('Join the Italian Base community and fully enjoy the Onchain Summer'); | ||
const mintInstructions = template.mintInstructions as EVMMintInstructions; | ||
|
||
expect(mintInstructions.contractAddress).to.equal('0x17ca8424F3c42aA840eB1B54a2400eC80330Eea8'); | ||
expect(mintInstructions.contractMethod).to.equal('mintWithComment'); | ||
expect(mintInstructions.contractParams).to.equal('[address, 1, ""]'); | ||
expect(mintInstructions.priceWei).to.equal('100000000000000'); | ||
|
||
expect(template.featuredImageUrl).to.equal( | ||
'https://ipfs.io/ipfs/Qme3w2B1W9zHsASbV7KRgdfrkHLFCWfhtQiDgeEYbzSHnb/nft-gallery-1gif', | ||
); | ||
|
||
if (template.creator) { | ||
expect(template.creator.walletAddress?.toLowerCase()).to.equal('0x30bec89100f144aad632153de93b58a32772cf58'); | ||
} | ||
|
||
expect(template.marketingUrl).to.equal(url); | ||
expect(template.availableForPurchaseStart?.getTime()).to.equal(+new Date('2024-07-24T22:02:16.000Z')); | ||
expect(template.availableForPurchaseEnd?.getTime()).to.greaterThan(+new Date('2030-01-01T00:00:00.000Z')); | ||
}); | ||
|
||
it('createMintTemplateForUrl: Returns a mint template for a supported URL with creator metadata', async function () { | ||
const ingestor = new CoinbaseWalletIngestor(); | ||
const url = 'https://wallet.coinbase.com/nft/mint/eip155:8453:erc721:0xf9aDb505EaadacCF170e48eE46Ee4d5623f777d7'; | ||
const resources = mintIngestorResources(); | ||
const template = await ingestor.createMintTemplateForUrl(resources, url); | ||
|
||
// Verify that the mint template passed validation | ||
const builder = new MintTemplateBuilder(template); | ||
builder.validateMintTemplate(); | ||
|
||
expect(template.name).to.equal('Onchain Summit 2024 San Francisco'); | ||
expect(template.description).to.contain( | ||
'Base community is bringing the Onchain Summit Billboard to life in San Francisco', | ||
); | ||
const mintInstructions = template.mintInstructions as EVMMintInstructions; | ||
|
||
expect(mintInstructions.contractAddress).to.equal('0xf9aDb505EaadacCF170e48eE46Ee4d5623f777d7'); | ||
expect(mintInstructions.contractMethod).to.equal('mintWithComment'); | ||
expect(mintInstructions.contractParams).to.equal('[address, 1, ""]'); | ||
expect(mintInstructions.priceWei).to.equal('800000000000000'); | ||
|
||
expect(template.featuredImageUrl).to.equal( | ||
'https://ipfs.io/ipfs/QmYuxDK8zkCF6taNTZuMSVAPbDDATZzdtVbHBSfPxCmT9J/nft-gallery-1png', | ||
); | ||
|
||
if (template.creator) { | ||
expect(template.creator.name).to.equal('onchainsummit.base.eth'); | ||
expect(template.creator.walletAddress?.toLowerCase()).to.equal('0x03489e02bf56b43a8e91287e8cfef76a7a6a9aa3'); | ||
} | ||
|
||
expect(template.marketingUrl).to.equal(url); | ||
expect(template.availableForPurchaseStart?.getTime()).to.equal(+new Date('2024-07-26T23:20:15.000Z')); | ||
expect(template.availableForPurchaseEnd?.getTime()).to.equal(+new Date('2024-08-31T15:20:19.000Z')); | ||
}); | ||
}); |