Skip to content

Commit

Permalink
Custom salt (#47)
Browse files Browse the repository at this point in the history
* custom salt validation

* Removes unused package

* Adding as salt option

* Using uuid as salt

* DEFAULT_SALT == uuid

* Removes unused function

* Moves error into uuidToBytes32

* keccak256 hash the salt

* Adds expectedEditionAddress

* changeset

* salt: string | number

* Remove await before returning promise

Co-authored-by: Vignesh Hirudayakanth <vigneshkanth@gmail.com>
  • Loading branch information
gigamesh and vigneshka committed Sep 8, 2022
1 parent df141a2 commit f6e54d0
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 41 deletions.
6 changes: 6 additions & 0 deletions .changeset/breezy-emus-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@soundxyz/sdk': minor
---

- Enables edition salt to be any string or number, which we hash & format to Bytes32
- Adds public expectedEditionAddress function
57 changes: 33 additions & 24 deletions packages/sdk/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { hexlify, hexZeroPad } from '@ethersproject/bytes'
import {
FixedPriceSignatureMinter__factory,
IMinterModule__factory,
Expand Down Expand Up @@ -32,9 +31,8 @@ import {
supportedChainIds,
supportedNetworks,
} from './utils/constants'
import { validateAddress } from './utils/helpers'
import { getSaltAsBytes32, validateAddress } from './utils/helpers'
import { LazyPromise } from './utils/promise'

import type { Signer } from '@ethersproject/abstract-signer'
import type { BigNumberish } from '@ethersproject/bignumber'
import type { ContractTransaction } from '@ethersproject/contracts'
Expand All @@ -60,8 +58,9 @@ export function SoundClient({
activeMintSchedules,
eligibleQuantity,
mint,
soundInfo,
createEditionWithMintSchedules,
soundInfo,
expectedEditionAddress,
}

// If the contract address is a SoundEdition contract
Expand Down Expand Up @@ -227,7 +226,7 @@ export function SoundClient({

switch (mintSchedule.interfaceId) {
case interfaceIds.IRangeEditionMinter: {
return await RangeEditionMinter__factory.connect(mintSchedule.minterAddress, signer).mint(
return RangeEditionMinter__factory.connect(mintSchedule.minterAddress, signer).mint(
mintSchedule.editionAddress,
mintSchedule.mintId,
quantity,
Expand All @@ -250,7 +249,7 @@ export function SoundClient({
})
}

return await merkleDropMinter.mint(
return merkleDropMinter.mint(
mintSchedule.editionAddress,
mintSchedule.mintId,
quantity,
Expand All @@ -272,19 +271,18 @@ export function SoundClient({
}: {
editionConfig: EditionConfig
mintConfigs: MintConfig[]
salt?: string
salt?: string | number
}) {
const { signer, chainId, userAddress } = await _requireSigner()

const randomInt = Math.floor(Math.random() * 1_000_000_000_000)
const salt = customSalt || hexZeroPad(hexlify(randomInt), 32)
const formattedSalt = getSaltAsBytes32(customSalt || Math.random() * 1_000_000_000_000_000)

const creatorAdddress = _getCreatorAddress(chainId)

// Precompute the edition address.
const editionAddress = await SoundCreatorV1__factory.connect(creatorAdddress, signer).soundEditionAddress(
userAddress,
salt,
formattedSalt,
)

const editionInterface = SoundEditionV1__factory.createInterface()
Expand Down Expand Up @@ -387,14 +385,37 @@ export function SoundClient({
const creatorAddress = _getCreatorAddress(chainId)
const soundCreatorContract = SoundCreatorV1__factory.connect(creatorAddress, signer)

return await soundCreatorContract.createSoundAndMints(
salt,
return soundCreatorContract.createSoundAndMints(
formattedSalt,
editionInitData,
contractCalls.map((d) => d.contractAddress),
contractCalls.map((d) => d.calldata),
)
}

async function soundInfo(soundParams: ReleaseInfoQueryVariables) {
const { data, errors } = await client.soundApi.releaseInfo(soundParams)

const release = data?.release
if (!release) throw new SoundNotFoundError({ ...soundParams, graphqlErrors: errors })

return {
...release,
trackAudio: LazyPromise(() => client.soundApi.audioFromTrack({ trackId: release.track.id })),
}
}

async function expectedEditionAddress({ deployer, salt }: { deployer: string; salt: string | number }) {
validateAddress(deployer)
const { signerOrProvider, chainId } = await _requireSignerOrProvider()
const soundCreatorAddress = _getCreatorAddress(chainId)

return SoundCreatorV1__factory.connect(soundCreatorAddress, signerOrProvider).soundEditionAddress(
deployer,
getSaltAsBytes32(salt),
)
}

/*********************************************************
INTERNAL FUNCTIONS
********************************************************/
Expand Down Expand Up @@ -541,18 +562,6 @@ export function SoundClient({
}
}

async function soundInfo(soundParams: ReleaseInfoQueryVariables) {
const { data, errors } = await client.soundApi.releaseInfo(soundParams)

const release = data?.release
if (!release) throw new SoundNotFoundError({ ...soundParams, graphqlErrors: errors })

return {
...release,
trackAudio: LazyPromise(() => client.soundApi.audioFromTrack({ trackId: release.track.id })),
}
}

function _isSupportedChain(chainId: number) {
return Object.values(supportedNetworks).includes(chainId as ChainId)
}
Expand Down
6 changes: 5 additions & 1 deletion packages/sdk/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { isAddress } from '@ethersproject/address'

import { InvalidAddressError } from '../errors'
import keccak256 from 'keccak256'

export function validateAddress(contractAddress: string) {
if (!isAddress(contractAddress)) {
throw new InvalidAddressError(`Invalid address: ${contractAddress}`)
}
}

export function getSaltAsBytes32(salt: string | number) {
return '0x' + keccak256(salt).toString('hex')
}
68 changes: 52 additions & 16 deletions packages/sdk/test/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import assert from 'assert'
import { expect } from 'chai'
import { ethers } from 'hardhat'

import { Wallet } from '@ethersproject/wallet'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
import {
Expand All @@ -17,8 +16,9 @@ import {
SoundFeeRegistry__factory,
} from '@soundxyz/sound-protocol/typechain/index'
import { interfaceIds } from '@soundxyz/sound-protocol'
import { getSaltAsBytes32 } from '../src/utils/helpers'
import { SoundClient } from '../src/client'
import { NotEligibleMint } from '../src/errors'
import { NotEligibleMint, InvalidAddressError, MissingSignerOrProviderError } from '../src/errors'
import { MINTER_ROLE } from '../src/utils/constants'
import { MerkleTestHelper, now } from './helpers'

Expand All @@ -33,8 +33,7 @@ const NON_NULL_ADDRESS = '0x0000000000000000000000000000000000000001'
const SOUND_FEE = 0
const ONE_HOUR = 3600
const PRICE = 420420420
const randomInt = Math.floor(Math.random() * 1_000_000_000_000)
const DEFAULT_SALT = ethers.utils.hexZeroPad(ethers.utils.hexlify(randomInt), 32)
const DEFAULT_SALT = getSaltAsBytes32(Math.random())

let client: SoundClient
let soundCreator: SoundCreatorV1
Expand Down Expand Up @@ -86,27 +85,26 @@ beforeEach(async () => {
artistWallet = signers[1]
buyerWallet = signers[2]

client = SoundClient({ provider: ethers.provider, apiKey: '123', environment: 'preview' })
const fixture = await loadFixture(deployProtocol)

soundCreator = fixture.soundCreator
precomputedEditionAddress = fixture.precomputedEditionAddress
fixedPriceSignatureMinter = fixture.fixedPriceSignatureMinter
merkleDropMinter = fixture.merkleDropMinter
rangeEditionMinter = fixture.rangeEditionMinter

client = SoundClient({
provider: ethers.provider,
apiKey: '123',
environment: 'preview',
soundCreatorAddress: soundCreator.address,
})
})

/**
* Sets up an edition and mint schedules.
*/
export async function setupTest({
customSalt,
minterCalls = [],
}: {
customSalt?: string
minterCalls?: ContractCall[]
}) {
const salt = customSalt || DEFAULT_SALT
export async function setupTest({ minterCalls = [] }: { minterCalls?: ContractCall[] }) {
const editionInitArgs = [
'Song Name',
'SYMBOL',
Expand All @@ -121,7 +119,7 @@ export async function setupTest({
]
const editionInterface = new ethers.utils.Interface(SoundEditionV1__factory.abi)
const editionInitData = editionInterface.encodeFunctionData('initialize', editionInitArgs)
const editionAddress = await soundCreator.soundEditionAddress(artistWallet.address, salt)
const editionAddress = await soundCreator.soundEditionAddress(artistWallet.address, DEFAULT_SALT)

const grantRolesCalls = [
{
Expand All @@ -141,7 +139,7 @@ export async function setupTest({
const allContractCalls = [...grantRolesCalls, ...minterCalls]

await soundCreator.connect(artistWallet).createSoundAndMints(
salt,
DEFAULT_SALT,
editionInitData,
allContractCalls.map((d) => d.contractAddress),
allContractCalls.map((d) => d.calldata),
Expand Down Expand Up @@ -671,13 +669,19 @@ describe('createEditionWithMintSchedules', () => {
},
]

const customSalt = 'hello world'
const precomputedEditionAddress = await SoundCreatorV1__factory.connect(
soundCreator.address,
ethers.provider,
).soundEditionAddress(artistWallet.address, getSaltAsBytes32(customSalt))

/**
* Create sound edition and mint schedules.
*/
await client.createEditionWithMintSchedules({
editionConfig,
mintConfigs,
salt: DEFAULT_SALT,
salt: customSalt,
})

const editionContract = SoundEditionV1__factory.connect(precomputedEditionAddress, ethers.provider)
Expand Down Expand Up @@ -728,3 +732,35 @@ describe('createEditionWithMintSchedules', () => {
}
})
})

describe('expectedEditionAddress', () => {
it('throws if provided deployerAddress is invalid', () => {
client.expectedEditionAddress({ deployer: '0x0', salt: '123' }).catch((error) => {
expect(error).instanceOf(InvalidAddressError)
})
})

it('throws if provider not connected', () => {
client = SoundClient({ provider: new ethers.providers.JsonRpcProvider(), apiKey: '123' })

client
.expectedEditionAddress({ deployer: '0xbf9a1fad0cbd61cc8158ccb6e1e8e111707088bb', salt: '123' })
.catch((error) => {
expect(error).instanceOf(MissingSignerOrProviderError)
})
})

it('returns expected address', async () => {
const deployer = artistWallet.address
const salt = '12345'

const expectedAddress = await SoundCreatorV1__factory.connect(
soundCreator.address,
ethers.provider,
).soundEditionAddress(deployer, getSaltAsBytes32(salt))

const address = await client.expectedEditionAddress({ deployer, salt })

expect(address).to.eq(expectedAddress)
})
})

0 comments on commit f6e54d0

Please sign in to comment.