Skip to content
This repository has been archived by the owner on Dec 13, 2024. It is now read-only.

feat: support NFT (sub account) transfers #149

Open
wants to merge 1 commit into
base: feature/dependency-funding
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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