Skip to content

Commit

Permalink
feat: support NFT (sub account) transfers
Browse files Browse the repository at this point in the history
  • Loading branch information
jtourkos committed Oct 24, 2023
1 parent d61df5f commit 7f1b5d9
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 4 deletions.
34 changes: 32 additions & 2 deletions src/NFTDriver/NFTDriverClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import type { Provider } from '@ethersproject/providers';
import type { BigNumberish, ContractTransaction, Signer } from 'ethers';
import { constants, BigNumber } from 'ethers';
import type { StreamReceiverStruct, SplitsReceiverStruct, AccountMetadata } from '../common/types';
import type { StreamReceiverStruct, SplitsReceiverStruct, AccountMetadata, Address } from '../common/types';
import type { NFTDriver } from '../../contracts';
import { NFTDriver__factory, IERC20__factory } from '../../contracts';
import { NFTDriver__factory, IERC20__factory, IERC721__factory } from '../../contracts';
import { DripsErrors } from '../common/DripsError';
import {
validateAddress,
Expand Down Expand Up @@ -148,6 +148,36 @@ export default class NFTDriverClient {
return signerAsErc20Contract.approve(this.#driverAddress, constants.MaxUint256);
}

/**
* @param from The current owner of the token.
* @param to The new owner.
* @param tokenId The token ID.
* @see {@link https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#IERC721-transferFrom-address-address-uint256-}
*/
public safeTransferFrom(from: Address, to: Address, tokenId: string): Promise<ContractTransaction> {
validateAddress(from);
validateAddress(to);

const signerAsErc721Contract = IERC721__factory.connect(this.#driverAddress, this.#signer);

return signerAsErc721Contract['safeTransferFrom(address,address,uint256)'](from, to, tokenId);
}

/**
* @param from The current owner of the token.
* @param to The new owner.
* @param tokenId The token ID.
* @see {@link https://docs.openzeppelin.com/contracts/4.x/api/token/erc721#IERC721-safeTransferFrom-address-address-uint256-}
*/
public transferFrom(from: Address, to: Address, tokenId: string): Promise<ContractTransaction> {
validateAddress(from);
validateAddress(to);

const signerAsErc721Contract = IERC721__factory.connect(this.#driverAddress, this.#signer);

return signerAsErc721Contract.transferFrom(from, to, tokenId);
}

/**
* Calculates the ID of the token minted with salt.
* @param minter The minter address of the token.
Expand Down
88 changes: 88 additions & 0 deletions src/abi/IERC721.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
[
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
}
]
98 changes: 96 additions & 2 deletions tests/NFTDriver/NFTDriverClient.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import NFTDriverClient from '../../src/NFTDriver/NFTDriverClient';
import Utils from '../../src/utils';
import * as validators from '../../src/common/validators';
import NFTDriverTxFactory from '../../src/NFTDriver/NFTDriverTxFactory';
import type { IERC20, NFTDriver } from '../../contracts';
import { NFTDriver__factory, IERC20__factory } from '../../contracts';
import type { IERC20, IERC721, NFTDriver } from '../../contracts';
import { NFTDriver__factory, IERC20__factory, IERC721__factory } from '../../contracts';
import type { StreamReceiverStruct, SplitsReceiverStruct, AccountMetadata } from '../../src/common/types';
import { DripsErrorCode } from '../../src/common/DripsError';

Expand Down Expand Up @@ -195,6 +195,100 @@ describe('NFTDriverClient', () => {
});
});

describe('safeTransferFrom()', () => {
it('should validate the address inputs', async () => {
// Arrange
const from = 'invalid address';
const validateAddressStub = sinon.stub(validators, 'validateAddress');
const to = Wallet.createRandom().address;
const tokenId = '1';

const erc721ContractStub = stubInterface<IERC721>();

sinon
.stub(IERC721__factory, 'connect')
.withArgs(testNftDriverClient.driverAddress, signerWithProviderStub)
.returns(erc721ContractStub);

// Act
await testNftDriverClient.safeTransferFrom(from, to, tokenId);

// Assert
assert(validateAddressStub.calledWith(from));
assert(validateAddressStub.calledWith(from));
});

it('should call the safeTransferFrom() method of the ERC721 contract', async () => {
// Arrange
const to = Wallet.createRandom().address;
const from = Wallet.createRandom().address;
const tokenId = '1';

const erc721ContractStub = stubInterface<IERC721>();

sinon
.stub(IERC721__factory, 'connect')
.withArgs(testNftDriverClient.driverAddress, signerWithProviderStub)
.returns(erc721ContractStub);

// Act
await testNftDriverClient.safeTransferFrom(from, to, tokenId);

// Assert
assert(
erc721ContractStub['safeTransferFrom(address,address,uint256)'].calledOnceWithExactly(from, to, tokenId),
'Expected method to be called with different arguments'
);
});
});

describe('transferFrom()', () => {
it('should validate the address inputs', async () => {
// Arrange
const from = 'invalid address';
const validateAddressStub = sinon.stub(validators, 'validateAddress');
const to = Wallet.createRandom().address;
const tokenId = '1';

const erc721ContractStub = stubInterface<IERC721>();

sinon
.stub(IERC721__factory, 'connect')
.withArgs(testNftDriverClient.driverAddress, signerWithProviderStub)
.returns(erc721ContractStub);

// Act
await testNftDriverClient.transferFrom(from, to, tokenId);

// Assert
assert(validateAddressStub.calledWith(from));
assert(validateAddressStub.calledWith(from));
});

it('should call the transferFrom() method of the ERC721 contract', async () => {
// Arrange
const to = Wallet.createRandom().address;
const from = Wallet.createRandom().address;
const tokenId = '1';

const erc721ContractStub = stubInterface<IERC721>();

sinon
.stub(IERC721__factory, 'connect')
.withArgs(testNftDriverClient.driverAddress, signerWithProviderStub)
.returns(erc721ContractStub);

// Act
await testNftDriverClient.transferFrom(from, to, tokenId);

// Assert
assert(
erc721ContractStub.transferFrom.calledOnceWithExactly(from, to, tokenId),
'Expected method to be called with different arguments'
);
});
});

describe('calcTokenIdWithSalt', () => {
it('should call the calcAccountId() method of the AddressDriver contract', async () => {
// Arrange
Expand Down

0 comments on commit 7f1b5d9

Please sign in to comment.