Skip to content

Commit 41bd540

Browse files
authored
Minters v2 (#215)
1 parent d107f2f commit 41bd540

File tree

12 files changed

+1107
-661
lines changed

12 files changed

+1107
-661
lines changed

.changeset/beige-cherries-wink.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@soundxyz/sdk': minor
3+
---
4+
5+
- Adds support for RangeEditionMinterV2 and MerkleDropMinterV2 contract interfaces
6+
- Adds mintTo function which is compatible with v2 contracts

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
},
1010
"dependencies": {
1111
"@soundxyz/sdk": "workspace:^",
12-
"@soundxyz/sound-protocol": "^1.4.0",
12+
"@soundxyz/sound-protocol": "^1.5.0",
1313
"alchemy-sdk": "^2.6.2",
1414
"date-fns": "^2.29.3",
1515
"ethers": "^5.7.2",

packages/sdk/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,9 @@
6363
"@nomicfoundation/hardhat-network-helpers": "^1.0.8",
6464
"@nomiclabs/hardhat-ethers": "^2.2.2",
6565
"@nomiclabs/hardhat-waffle": "^2.0.5",
66-
"@soundxyz/sound-protocol": "^1.4.0",
6766
"@soundxyz/sound-protocol-v1-0": "npm:@soundxyz/sound-protocol@1.1.0",
6867
"@soundxyz/sound-protocol-v1-1": "npm:@soundxyz/sound-protocol@1.3.0",
68+
"@soundxyz/sound-protocol": "^1.5.0",
6969
"@types/chai": "^4.3.4",
7070
"@types/mocha": "^10.0.1",
7171
"@types/node": "18.15.13",
@@ -87,7 +87,7 @@
8787
"typescript": "5.0.3"
8888
},
8989
"peerDependencies": {
90-
"@soundxyz/sound-protocol": "~1.4.0"
90+
"@soundxyz/sound-protocol": "~1.5.0"
9191
},
9292
"publishConfig": {
9393
"access": "public",

packages/sdk/src/client/edition/create.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { BigNumberish } from '@ethersproject/bignumber'
22
import { Overrides } from '@ethersproject/contracts'
33
import {
4-
MerkleDropMinter__factory,
5-
RangeEditionMinter__factory,
4+
MerkleDropMinterV2__factory,
5+
RangeEditionMinterV2__factory,
66
SAM__factory,
77
SoundCreatorV1__factory,
88
SoundEditionV1_2__factory,
@@ -47,7 +47,7 @@ export async function createEdition(
4747
notNull: true,
4848
})
4949

50-
await validateEditionConfig(editionConfig)
50+
validateEditionConfig(editionConfig)
5151

5252
validateMintConfigs(mintConfigs)
5353

@@ -61,12 +61,6 @@ export async function createEdition(
6161

6262
const formattedSalt = getSaltAsBytes32(customSalt || Math.random() * 1_000_000_000_000_000)
6363

64-
validateAddress({
65-
address: creatorAddress,
66-
type: 'CREATOR_ADDRESS',
67-
notNull: true,
68-
})
69-
7064
// Precompute the edition address.
7165
const [editionAddress, _] = await SoundCreatorV1__factory.connect(creatorAddress, signer).soundEditionAddress(
7266
userAddress,
@@ -96,7 +90,7 @@ export async function createEdition(
9690
*/
9791
switch (mintConfig.mintType) {
9892
case 'RangeEdition': {
99-
const minterInterface = RangeEditionMinter__factory.createInterface()
93+
const minterInterface = RangeEditionMinterV2__factory.createInterface()
10094
contractCalls.push({
10195
contractAddress: mintConfig.minterAddress,
10296

@@ -115,7 +109,7 @@ export async function createEdition(
115109
break
116110
}
117111
case 'MerkleDrop': {
118-
const minterInterface = MerkleDropMinter__factory.createInterface()
112+
const minterInterface = MerkleDropMinterV2__factory.createInterface()
119113
contractCalls.push({
120114
contractAddress: mintConfig.minterAddress,
121115
calldata: minterInterface.encodeFunctionData('createEditionMint', [
@@ -194,7 +188,7 @@ export async function createEdition(
194188
)
195189
}
196190

197-
export async function validateEditionConfig(config: EditionConfig) {
191+
export function validateEditionConfig(config: EditionConfig) {
198192
const { editionMaxMintableLower, editionMaxMintableUpper, fundingRecipient, metadataModule, setSAM } = config
199193

200194
validateAddress({

packages/sdk/src/client/edition/mint.ts

Lines changed: 161 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { BigNumber } from '@ethersproject/bignumber'
22
import { ContractTransaction, PayableOverrides } from '@ethersproject/contracts'
3+
import { SoundEditionV1_2__factory } from '@soundxyz/sound-protocol/typechain'
4+
5+
import { InvalidAttributonIdError, InvalidQuantityError, NotEligibleMint } from '../../errors'
6+
import { MintOptions, MintSchedule, MintToOptions } from '../../types'
37
import {
4-
MerkleDropMinter__factory,
5-
RangeEditionMinter__factory,
6-
SoundEditionV1_2__factory,
7-
} from '@soundxyz/sound-protocol/typechain'
8-
9-
import { InvalidQuantityError, NotEligibleMint } from '../../errors'
10-
import { MintOptions, MintSchedule } from '../../types'
11-
import { MINT_FALLBACK_GAS_LIMIT, MINT_GAS_LIMIT_MULTIPLIER, NULL_ADDRESS } from '../../utils/constants'
12-
import { scaleAmount } from '../../utils/helpers'
8+
MINT_FALLBACK_GAS_LIMIT,
9+
MINT_GAS_LIMIT_MULTIPLIER,
10+
minterFactoryMap,
11+
NULL_ADDRESS,
12+
} from '../../utils/constants'
13+
import { exhaustiveGuard, scaleAmount } from '../../utils/helpers'
1314
import { SoundClientInstance } from '../instance'
1415
import { validateSoundEdition } from '../validation'
1516
import { getMerkleProof } from './merkle'
1617
import { isSchedulePaused } from './schedules'
18+
import { interfaceIds } from '@soundxyz/sound-protocol/interfaceIds'
19+
import { isBigNumberish } from '@ethersproject/bignumber/lib/bignumber'
1720

1821
export async function numberOfTokensOwned(
1922
this: SoundClientInstance,
@@ -69,9 +72,13 @@ export async function mint(
6972
maxPriorityFeePerGas,
7073
}
7174

72-
switch (mintSchedule.mintType) {
73-
case 'RangeEdition': {
74-
const rangeMinter = RangeEditionMinter__factory.connect(mintSchedule.minterAddress, signer)
75+
const interfaceId = mintSchedule.interfaceId
76+
77+
switch (interfaceId) {
78+
case interfaceIds.IRangeEditionMinter:
79+
case interfaceIds.IRangeEditionMinterV2: {
80+
const rangeMinter = minterFactoryMap[interfaceId].connect(mintSchedule.minterAddress, signer)
81+
7582
const mintArgs = [mintSchedule.editionAddress, mintSchedule.mintId, quantity, affiliate] as const
7683

7784
if (txnOverrides.gasLimit) {
@@ -91,8 +98,9 @@ export async function mint(
9198
return rangeMinter.mint(...mintArgs, txnOverrides)
9299
}
93100

94-
case 'MerkleDrop': {
95-
const merkleDropMinter = MerkleDropMinter__factory.connect(mintSchedule.minterAddress, signer)
101+
case interfaceIds.IMerkleDropMinter:
102+
case interfaceIds.IMerkleDropMinterV2: {
103+
const merkleDropMinter = minterFactoryMap[interfaceId].connect(mintSchedule.minterAddress, signer)
96104

97105
const { merkleRootHash: merkleRoot } = await merkleDropMinter.mintInfo(
98106
mintSchedule.editionAddress,
@@ -131,8 +139,145 @@ export async function mint(
131139
return merkleDropMinter.mint(...mintArgs, txnOverrides)
132140
}
133141

134-
default:
135-
throw new Error('Unimplemented')
142+
default: {
143+
exhaustiveGuard(interfaceId)
144+
}
145+
}
146+
}
147+
148+
export async function mintTo(
149+
this: SoundClientInstance,
150+
{
151+
affiliate = NULL_ADDRESS,
152+
affiliateProof = [],
153+
attributonId = 0,
154+
gasLimit,
155+
maxFeePerGas,
156+
maxPriorityFeePerGas,
157+
mintSchedule,
158+
mintToAddress,
159+
quantity,
160+
}: MintToOptions,
161+
): Promise<ContractTransaction> {
162+
if (!isBigNumberish(attributonId)) {
163+
throw new InvalidAttributonIdError({
164+
attributonId,
165+
})
166+
}
167+
168+
await validateSoundEdition.call(this, { editionAddress: mintSchedule.editionAddress })
169+
if (quantity <= 0 || Math.floor(quantity) !== quantity) throw new InvalidQuantityError({ quantity })
170+
171+
const { signer, userAddress } = await this.expectSigner()
172+
173+
const toAddress = mintToAddress ?? userAddress
174+
175+
const eligibleMintQuantity = await eligibleQuantity.call(this, {
176+
mintSchedule,
177+
userAddress,
178+
})
179+
if (eligibleMintQuantity < quantity) {
180+
throw new NotEligibleMint({
181+
eligibleMintQuantity,
182+
mintSchedule,
183+
userAddress,
184+
})
185+
}
186+
187+
const txnOverrides: PayableOverrides = {
188+
value: 'price' in mintSchedule ? mintSchedule.price.mul(quantity) : BigNumber.from('0'),
189+
gasLimit,
190+
maxFeePerGas,
191+
maxPriorityFeePerGas,
192+
}
193+
194+
const interfaceId = mintSchedule.interfaceId
195+
196+
switch (interfaceId) {
197+
case interfaceIds.IRangeEditionMinterV2: {
198+
const rangeMinter = minterFactoryMap[interfaceId].connect(mintSchedule.minterAddress, signer)
199+
200+
const mintArgs = [
201+
mintSchedule.editionAddress,
202+
mintSchedule.mintId,
203+
toAddress,
204+
quantity,
205+
affiliate,
206+
affiliateProof,
207+
attributonId,
208+
] as const
209+
210+
if (txnOverrides.gasLimit) {
211+
return rangeMinter.mintTo(...mintArgs, txnOverrides)
212+
}
213+
214+
try {
215+
// Add a buffer to the gas estimate to account for node provider estimate variance.
216+
const gasEstimate = await rangeMinter.estimateGas.mintTo(...mintArgs, txnOverrides)
217+
218+
txnOverrides.gasLimit = scaleAmount({ amount: gasEstimate, multiplier: MINT_GAS_LIMIT_MULTIPLIER })
219+
} catch (err) {
220+
// If estimation fails, provide a hardcoded gas limit that is guaranteed to succeed.
221+
txnOverrides.gasLimit = MINT_FALLBACK_GAS_LIMIT
222+
}
223+
224+
return rangeMinter.mintTo(...mintArgs, txnOverrides)
225+
}
226+
227+
case interfaceIds.IMerkleDropMinterV2: {
228+
const merkleDropMinter = minterFactoryMap[interfaceId].connect(mintSchedule.minterAddress, signer)
229+
230+
const { merkleRootHash: merkleRoot } = await merkleDropMinter.mintInfo(
231+
mintSchedule.editionAddress,
232+
mintSchedule.mintId,
233+
)
234+
235+
const proof = await getMerkleProof.call(this, {
236+
merkleRoot,
237+
userAddress,
238+
})
239+
240+
if (!proof?.length) {
241+
throw new NotEligibleMint({
242+
mintSchedule,
243+
userAddress,
244+
eligibleMintQuantity,
245+
})
246+
}
247+
248+
const mintArgs = [
249+
mintSchedule.editionAddress,
250+
mintSchedule.mintId,
251+
toAddress,
252+
quantity,
253+
// TODO, allow overriding allowlisted for delegatecash
254+
toAddress,
255+
proof,
256+
affiliate,
257+
affiliateProof,
258+
attributonId,
259+
] as const
260+
261+
if (txnOverrides.gasLimit) {
262+
return merkleDropMinter.mintTo(...mintArgs, txnOverrides)
263+
}
264+
265+
try {
266+
// Add a buffer to the gas estimate to account for node provider estimate variance.
267+
const gasEstimate = await merkleDropMinter.estimateGas.mintTo(...mintArgs, txnOverrides)
268+
269+
txnOverrides.gasLimit = scaleAmount({ amount: gasEstimate, multiplier: MINT_GAS_LIMIT_MULTIPLIER })
270+
} catch (err) {
271+
// If estimation fails, provide a hardcoded gas limit that is guaranteed to succeed.
272+
txnOverrides.gasLimit = MINT_FALLBACK_GAS_LIMIT
273+
}
274+
275+
return merkleDropMinter.mintTo(...mintArgs, txnOverrides)
276+
}
277+
278+
default: {
279+
exhaustiveGuard(interfaceId)
280+
}
136281
}
137282
}
138283

packages/sdk/src/client/edition/schedules.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { interfaceIds } from '@soundxyz/sound-protocol'
2-
import { IMinterModule__factory, SoundEditionV1_2__factory } from '@soundxyz/sound-protocol/typechain'
2+
import { IMinterModuleV2__factory, SoundEditionV1_2__factory } from '@soundxyz/sound-protocol/typechain'
33

44
import { UnsupportedMinterError } from '../../errors'
5-
import { BlockOrBlockHash, MinterInterfaceId, MintSchedule } from '../../types'
5+
import { BlockOrBlockHash, HANDLED_MINTER_INTERFACE_IDS, MinterInterfaceId, MintSchedule } from '../../types'
66
import { minterFactoryMap } from '../../utils/constants'
77
import { LazyPromise } from '../../utils/promise'
88
import { SoundClientInstance } from '../instance'
9+
import { exhaustiveGuard } from '../../utils/helpers'
910

1011
export async function mintSchedules(
1112
this: SoundClientInstance,
@@ -122,12 +123,14 @@ export async function editionRegisteredMinters(
122123
// Check supportsInterface() to verify each address is a minter
123124
const minters = await Promise.all(
124125
candidateMinters.map(async (minterAddress) => {
125-
const minterContract = IMinterModule__factory.connect(minterAddress, signerOrProvider)
126+
const minterContract = IMinterModuleV2__factory.connect(minterAddress, signerOrProvider)
126127

127128
try {
128-
const isMinter = await minterContract.supportsInterface(interfaceIds.IMinterModule)
129+
const moduleInterfaceId = await minterContract.moduleInterfaceId()
129130

130-
return isMinter ? minterAddress : null
131+
return HANDLED_MINTER_INTERFACE_IDS.indexOf(moduleInterfaceId as MinterInterfaceId) !== -1
132+
? minterAddress
133+
: null
131134
} catch (err) {
132135
onUnhandledError(err)
133136
return null
@@ -157,8 +160,8 @@ export async function editionMinterMintIds(
157160
) {
158161
const { signerOrProvider } = await this.expectSignerOrProvider()
159162

160-
// Query MintConfigCreated event
161-
const minterContract = IMinterModule__factory.connect(minterAddress, signerOrProvider)
163+
// Query MintConfigCreated event, for v1 and v2, this signature is the same
164+
const minterContract = IMinterModuleV2__factory.connect(minterAddress, signerOrProvider)
162165
const filter = minterContract.filters.MintConfigCreated(editionAddress)
163166
const mintScheduleConfigEvents = await minterContract.queryFilter(filter, fromBlockOrBlockHash)
164167
return mintScheduleConfigEvents.map((event) => event.args.mintId.toNumber())
@@ -187,15 +190,20 @@ export async function mintInfosFromMinter(
187190
const signerOrProvider = LazyPromise(() => expectSignerOrProvider().then((v) => v.signerOrProvider))
188191

189192
const interfaceId = await idempotentCachedCall(`minter-interface-id-${minterAddress}`, async () => {
190-
const minterModule = IMinterModule__factory.connect(minterAddress, await signerOrProvider)
193+
const minterModule = IMinterModuleV2__factory.connect(minterAddress, await signerOrProvider)
191194

192195
return (await minterModule.moduleInterfaceId()) as MinterInterfaceId
193196
})
194197

195198
return Promise.all(
196199
mintIds.map(async (mintId) => {
200+
if (HANDLED_MINTER_INTERFACE_IDS.indexOf(interfaceId) === -1) {
201+
throw new UnsupportedMinterError({ interfaceId })
202+
}
203+
197204
switch (interfaceId) {
198-
case interfaceIds.IRangeEditionMinter: {
205+
case interfaceIds.IRangeEditionMinter:
206+
case interfaceIds.IRangeEditionMinterV2: {
199207
const minterContract = minterFactoryMap[interfaceId].connect(minterAddress, await signerOrProvider)
200208
const mintSchedule = await minterContract.mintInfo(editionAddress, mintId)
201209
return {
@@ -220,11 +228,14 @@ export async function mintInfosFromMinter(
220228
affiliateFeeBPS: mintSchedule.affiliateFeeBPS,
221229
}
222230
}
223-
case interfaceIds.IMerkleDropMinter: {
231+
232+
case interfaceIds.IMerkleDropMinter:
233+
case interfaceIds.IMerkleDropMinterV2: {
224234
const minterContract = minterFactoryMap[interfaceId].connect(minterAddress, await signerOrProvider)
225235
const mintSchedule = await minterContract.mintInfo(editionAddress, mintId)
226236
return {
227237
mintType: 'MerkleDrop',
238+
interfaceId,
228239
mintId,
229240
merkleRoot: mintSchedule.merkleRootHash,
230241
editionAddress,
@@ -240,7 +251,7 @@ export async function mintInfosFromMinter(
240251
}
241252
}
242253
default: {
243-
throw new UnsupportedMinterError({ interfaceId })
254+
exhaustiveGuard(interfaceId)
244255
}
245256
}
246257
}),

0 commit comments

Comments
 (0)