diff --git a/.lintstagedrc.js b/.lintstagedrc.js index 03b9361a..cedff90e 100644 --- a/.lintstagedrc.js +++ b/.lintstagedrc.js @@ -3,6 +3,6 @@ module.exports = { `eslint --cache --fix ${filenames.join(' ')}`, 'tsc -p tsconfig.json --noEmit', 'typedoc', - 'git add docs', + 'git add docs/api', ], } diff --git a/docs/api/README.md b/docs/api/README.md index c1c75fbe..5b8bf5bc 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -9,7 +9,11 @@ - [ColonyRole](enums/ColonyRole.md) - [Extension](enums/Extension.md) - [Id](enums/Id.md) +- [MetaTxBroadCasterEndpoint](enums/MetaTxBroadCasterEndpoint.md) +- [MetadataType](enums/MetadataType.md) - [MotionState](enums/MotionState.md) +- [Network](enums/Network.md) +- [TeamColor](enums/TeamColor.md) - [Vote](enums/Vote.md) ## Classes @@ -19,20 +23,26 @@ - [ColonyEventManager](classes/ColonyEventManager.md) - [ColonyNetwork](classes/ColonyNetwork.md) - [ColonyToken](classes/ColonyToken.md) -- [MotionCreator](classes/MotionCreator.md) - [PinataAdapter](classes/PinataAdapter.md) +- [TxCreator](classes/TxCreator.md) - [VotingReputation](classes/VotingReputation.md) ## Interfaces +- [AnnotationMetadata](interfaces/AnnotationMetadata.md) - [ColonyEvent](interfaces/ColonyEvent.md) - [ColonyEventManagerOptions](interfaces/ColonyEventManagerOptions.md) - [ColonyFilter](interfaces/ColonyFilter.md) - [ColonyMultiFilter](interfaces/ColonyMultiFilter.md) +- [ColonyNetworkOptions](interfaces/ColonyNetworkOptions.md) +- [ColonyTopic](interfaces/ColonyTopic.md) - [Ethers6Filter](interfaces/Ethers6Filter.md) - [Ethers6FilterByBlockHash](interfaces/Ethers6FilterByBlockHash.md) - [EventSources](interfaces/EventSources.md) - [IpfsAdapter](interfaces/IpfsAdapter.md) +- [NetworkClientOptions](interfaces/NetworkClientOptions.md) +- [ParsedLogTransactionReceipt](interfaces/ParsedLogTransactionReceipt.md) +- [SupportedExtensions](interfaces/SupportedExtensions.md) ## Type Aliases @@ -46,19 +56,13 @@ ___ ### MetadataEvent -Ƭ **MetadataEvent**: keyof typeof `IPFS_METADATA_EVENTS` - -___ - -### MetadataEventValue - -Ƭ **MetadataEventValue**<`K`\>: `ReturnType` +Ƭ **MetadataEvent**<`K`\>: typeof `IPFS_METADATA_EVENTS`[`K`] #### Type parameters | Name | Type | | :------ | :------ | -| `K` | extends [`MetadataEvent`](README.md#metadataevent) | +| `K` | extends [`MetadataType`](enums/MetadataType.md) | ___ @@ -70,7 +74,7 @@ ___ | Name | Type | | :------ | :------ | -| `K` | extends `MetadataType` | +| `K` | extends [`MetadataType`](enums/MetadataType.md) | ## Functions @@ -93,11 +97,11 @@ Check if two addresses are equal ___ -### extractEvent +### extractCustomEvent -▸ **extractEvent**<`T`\>(`eventName`, `receipt`): `undefined` \| `T` +▸ **extractCustomEvent**<`T`\>(`eventName`, `receipt`, `iface`): `undefined` \| `T` -Extract event args from a contract receipt +Manually extract an event using the interface (e.g. if emitting contract is a different one than the calling contract) #### Type parameters @@ -110,7 +114,8 @@ Extract event args from a contract receipt | Name | Type | | :------ | :------ | | `eventName` | `string` | -| `receipt` | `ContractReceipt` | +| `receipt` | `ContractReceipt` \| [`ParsedLogTransactionReceipt`](interfaces/ParsedLogTransactionReceipt.md) | +| `iface` | `Interface` | #### Returns @@ -118,11 +123,11 @@ Extract event args from a contract receipt ___ -### extractEventFromLogs +### extractEvent -▸ **extractEventFromLogs**<`T`\>(`eventName`, `receipt`, `iface`): `undefined` \| `T` +▸ **extractEvent**<`T`\>(`eventName`, `receipt`): `undefined` \| `T` -Manually extract an event from logs (e.g. if emitting contract is a different one than the calling contract) +Extract event args from a contract receipt #### Type parameters @@ -135,8 +140,7 @@ Manually extract an event from logs (e.g. if emitting contract is a different on | Name | Type | | :------ | :------ | | `eventName` | `string` | -| `receipt` | `ContractReceipt` | -| `iface` | `Interface` | +| `receipt` | `ContractReceipt` \| [`ParsedLogTransactionReceipt`](interfaces/ParsedLogTransactionReceipt.md) | #### Returns diff --git a/docs/api/classes/Colony.md b/docs/api/classes/Colony.md index 56e2614d..60246efc 100644 --- a/docs/api/classes/Colony.md +++ b/docs/api/classes/Colony.md @@ -16,7 +16,7 @@ ___ ### colonyToken -• **colonyToken**: [`ColonyToken`](ColonyToken.md) +• `Optional` **colonyToken**: [`ColonyToken`](ColonyToken.md) An instance of the Colony's native token @@ -26,7 +26,13 @@ ___ ### ext -• **ext**: `SupportedExtensions` +• **ext**: [`SupportedExtensions`](../interfaces/SupportedExtensions.md) + +___ + +### signerOrProvider + +• **signerOrProvider**: `SignerOrProvider` ___ @@ -47,7 +53,7 @@ If this is not an option, Colony SDK might throw errors at certain points. Usage ### annotateTransaction -▸ **annotateTransaction**(`txHash`, `annotationMsg`): `Promise`<[{ `agent?`: `string` ; `metadata?`: `string` ; `txHash?`: `string` }, `ContractReceipt`]\> +▸ **annotateTransaction**(`txHash`, `annotationMetadata`): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"annotateTransaction"``, { `agent?`: `string` ; `metadata?`: `string` ; `txHash?`: `string` }, [`Annotation`](../enums/MetadataType.md#annotation)\> Annotate a transaction with IPFS metadata to provide extra information @@ -55,7 +61,7 @@ This will annotate a transaction with an arbitrary text message. This only reall **`Remarks`** -Requires an [IpfsAdapter](../interfaces/IpfsAdapter.md) that can upload and pin to IPFS. See its documentation for more information. Keep in mind that **the annotation itself is a transaction**. +If [AnnotationMetadata](../interfaces/AnnotationMetadata.md) is provided directly (as opposed to a [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file) this requires an [IpfsAdapter](../interfaces/IpfsAdapter.md) that can upload and pin to IPFS. See its documentation for more information. Keep in mind that **the annotation itself is a transaction**. **`Example`** @@ -64,15 +70,16 @@ Requires an [IpfsAdapter](../interfaces/IpfsAdapter.md) that can upload and pin (async function() { // Create a motion to pay 10 of the native token to some (maybe your own?) address - const [, { transactionHash }] = await colony.ext.motions.create.pay( + // (forced transaction example) + const [, { transactionHash }] = await colony.ext.oneTx.pay( '0xb77D57F4959eAfA0339424b83FcFaf9c15407461', w`10`, - ); + ).motion(); // Annotate the motion transaction with a little explanation :) await colony.annotateTransaction( transactionHash, - 'I am creating this motion because I think I deserve a little bonus' - ); + { annotationMsg: 'I am creating this motion because I think I deserve a little bonus' }, + ).force(); })(); ``` @@ -81,13 +88,13 @@ Requires an [IpfsAdapter](../interfaces/IpfsAdapter.md) that can upload and pin | Name | Type | Description | | :------ | :------ | :------ | | `txHash` | `string` | Transaction hash of the transaction to annotate (within the Colony) | -| `annotationMsg` | `string` | The text message you would like to annotate the transaction with | +| `annotationMetadata` | `string` \| [`AnnotationMetadata`](../interfaces/AnnotationMetadata.md) | The annotation metadata you would like to annotate the transaction with (or an IPFS CID pointing to valid metadata) | #### Returns -`Promise`<[{ `agent?`: `string` ; `metadata?`: `string` ; `txHash?`: `string` }, `ContractReceipt`]\> +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"annotateTransaction"``, { `agent?`: `string` ; `metadata?`: `string` ; `txHash?`: `string` }, [`Annotation`](../enums/MetadataType.md#annotation)\> -A tupel of event data and contract receipt +A [TxCreator](TxCreator.md) **Event data** @@ -101,23 +108,27 @@ ___ ### claimFunds -▸ **claimFunds**(`tokenAddress?`): `Promise`<[{ `fee?`: `BigNumber` ; `payoutRemainder?`: `BigNumber` ; `token?`: `string` }, `ContractReceipt`]\> +▸ **claimFunds**(`tokenAddress?`): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"claimColonyFunds"``, { `fee?`: `BigNumber` ; `payoutRemainder?`: `BigNumber` ; `token?`: `string` }, [`MetadataType`](../enums/MetadataType.md)\> Claim outstanding Colony funds Anyone can call this function. Claims funds _for_ the Colony that have been sent to the Colony's contract address or minted funds of the Colony's native token. This function _has_ to be called in order for the funds to appear in the Colony's treasury. You can provide a token address for the token to be claimed. Otherwise it will claim the outstanding funds of the Colony's native token +**`Remarks`** + +use `ethers.constants.AddressZero` to claim ETH. + #### Parameters | Name | Type | Description | | :------ | :------ | :------ | -| `tokenAddress` | `string` | The address of the token to claim the funds for. Default is the Colony's native token | +| `tokenAddress?` | `string` | The address of the token to claim the funds for. Default is the Colony's native token | #### Returns -`Promise`<[{ `fee?`: `BigNumber` ; `payoutRemainder?`: `BigNumber` ; `token?`: `string` }, `ContractReceipt`]\> +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"claimColonyFunds"``, { `fee?`: `BigNumber` ; `payoutRemainder?`: `BigNumber` ; `token?`: `string` }, [`MetadataType`](../enums/MetadataType.md)\> -A tupel of event data and contract receipt +A [TxCreator](TxCreator.md) **Event data** @@ -132,25 +143,43 @@ ___ ### createTeam -▸ **createTeam**(`metadataCid?`): `Promise`<[{ `agent?`: `string` ; `domainId?`: `BigNumber` ; `fundingPotId?`: `BigNumber` ; `metadata?`: `string` }, `ContractReceipt`, () => `Promise`<`undefined` \| `DomainMetadata`\>] \| [{ `agent?`: `string` ; `domainId?`: `BigNumber` ; `fundingPotId?`: `BigNumber` ; `metadata?`: `string` }, `ContractReceipt`]\> +▸ **createTeam**(`metadata`): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"addDomain(uint256,uint256,uint256,string)"``, { `domainId`: `BigNumber` ; `fundingPotId`: `BigNumber` ; `metadata`: `string` }, [`Domain`](../enums/MetadataType.md#domain)\> -Create a team within a Colony +Create a team (domain) within a Colony with team details as metadata **`Remarks`** Currently you can only add domains within the `Root` domain. This restriction will be lifted soon +**`Example`** + +```typescript +import { TeamColor } from '@colony/sdk'; + +// Immediately executing async function +(async function() { + // Create team of the butter-passers + // (forced transaction example) + // (also notice that this requires an upload-capable IPFS adapter) + await colony.createTeam({ + domainName: 'Butter-passers', + domainColor: TeamColor.Gold, + domainPurpose: 'To pass butter', + }).force(); +})(); +``` + #### Parameters | Name | Type | Description | | :------ | :------ | :------ | -| `metadataCid?` | `string` | An IPFS [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file containing the metadata described below. For now, we would like to keep it agnostic to any IPFS upload mechanism, so you have to upload the file manually and provide your own hash (by using, for example, [Pinata](https://docs.pinata.cloud/)) | +| `metadata` | `string` \| `DomainMetadata` | The team metadata you would like to add (or an IPFS CID pointing to valid metadata). If DomainMetadata is provided directly (as opposed to a [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file) this requires an [IpfsAdapter](../interfaces/IpfsAdapter.md) that can upload and pin to IPFS (like the [PinataAdapter](PinataAdapter.md)). See its documentation for more information. | #### Returns -`Promise`<[{ `agent?`: `string` ; `domainId?`: `BigNumber` ; `fundingPotId?`: `BigNumber` ; `metadata?`: `string` }, `ContractReceipt`, () => `Promise`<`undefined` \| `DomainMetadata`\>] \| [{ `agent?`: `string` ; `domainId?`: `BigNumber` ; `fundingPotId?`: `BigNumber` ; `metadata?`: `string` }, `ContractReceipt`]\> +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"addDomain(uint256,uint256,uint256,string)"``, { `domainId`: `BigNumber` ; `fundingPotId`: `BigNumber` ; `metadata`: `string` }, [`Domain`](../enums/MetadataType.md#domain)\> -A tupel: `[eventData, ContractReceipt, getMetaData]` +A [TxCreator](TxCreator.md) **Event data** @@ -169,6 +198,59 @@ A tupel: `[eventData, ContractReceipt, getMetaData]` | `domainColor` | string | The color assigned to this team | | `domainPurpose` | string | The purpose for this team (a broad description) | +▸ **createTeam**(): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"addDomain(uint256,uint256,uint256,string)"``, { `domainId`: `BigNumber` ; `fundingPotId`: `BigNumber` ; `metadata`: `undefined` }, [`MetadataType`](../enums/MetadataType.md)\> + +Create a team (domain) within a Colony with no metadata attached + +**`Remarks`** + +Currently you can only add domains within the `Root` domain. This restriction will be lifted soon + +#### Returns + +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"addDomain(uint256,uint256,uint256,string)"``, { `domainId`: `BigNumber` ; `fundingPotId`: `BigNumber` ; `metadata`: `undefined` }, [`MetadataType`](../enums/MetadataType.md)\> + +A [TxCreator](TxCreator.md) + +**Event data** + +| Property | Type | Description | +| :------ | :------ | :------ | +| `agent` | string | The address that is responsible for triggering this event | +| `domainId` | BigNumber | Integer domain id of the created team | +| `fundingPotId` | BigNumber | Integer id of the corresponding funding pot | + +___ + +### deprecateTeam + +▸ **deprecateTeam**(`teamId`, `deprecated`): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"deprecateDomain"``, { `agent?`: `string` ; `deprecated?`: `boolean` ; `domainId?`: `BigNumber` }, [`MetadataType`](../enums/MetadataType.md)\> + +Deprecate (remove) or undeprecate a team + +Teams can be deprecated which will remove them from the UI. As they can't be deleted you can always undeprecate a team by passing `false` as the second argument. + +#### Parameters + +| Name | Type | Description | +| :------ | :------ | :------ | +| `teamId` | `BigNumberish` | Team to be (un)deprecated | +| `deprecated` | `boolean` | `true`: Deprecate team; `false`: Undeprecate team | + +#### Returns + +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"deprecateDomain"``, { `agent?`: `string` ; `deprecated?`: `boolean` ; `domainId?`: `BigNumber` }, [`MetadataType`](../enums/MetadataType.md)\> + +A [TxCreator](TxCreator.md) + +**Event data** + +| Property | Type | Description | +| :------ | :------ | :------ | +| `agent` | string | The address that is responsible for triggering this event | +| `domainId` | BigNumber | The id of the team that was (un)deprecated | +| `deprecated` | bool | Whether the team was deprecated or not | + ___ ### getBalance @@ -281,7 +363,7 @@ ___ ### makeArbitraryTransaction -▸ **makeArbitraryTransaction**(`target`, `action`): `Promise`<[{}, `ContractReceipt`]\> +▸ **makeArbitraryTransaction**(`target`, `action`): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"makeArbitraryTransactions"``, `Record`<`string`, `unknown`\>, [`MetadataType`](../enums/MetadataType.md)\> Execute an arbitrary transaction in the name of the Colony @@ -293,7 +375,8 @@ Mint an NFT from a Colony ```typescript import { ERC721 } from '@colony/sdk'; -// Mint a NFT for address 0xb794f5ea0ba39494ce839613fffba74279579268 +// Mint an NFT for address 0xb794f5ea0ba39494ce839613fffba74279579268 +// (forced transaction example) const encodedAction = ERC721.encodeFunctionData( 'mintTo', '0xb794f5ea0ba39494ce839613fffba74279579268', @@ -306,7 +389,7 @@ const encodedAction = ERC721.encodeFunctionData( '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d', // encoded transaction from above encodedAction - ); + ).force(); })(); ``` @@ -319,9 +402,9 @@ const encodedAction = ERC721.encodeFunctionData( #### Returns -`Promise`<[{}, `ContractReceipt`]\> +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"makeArbitraryTransactions"``, `Record`<`string`, `unknown`\>, [`MetadataType`](../enums/MetadataType.md)\> -A tupel of event data and contract receipt +A [TxCreator](TxCreator.md) **No event data** @@ -329,7 +412,7 @@ ___ ### moveFundsToTeam -▸ **moveFundsToTeam**(`amount`, `toTeam`, `fromTeam?`, `tokenAddress?`): `Promise`<[{ `agent?`: `string` ; `amount?`: `BigNumber` ; `fromPot?`: `BigNumber` ; `toPot?`: `BigNumber` ; `token?`: `string` }, `ContractReceipt`]\> +▸ **moveFundsToTeam**(`amount`, `toTeam`, `fromTeam?`, `tokenAddress?`): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)"``, { `agent?`: `string` ; `amount?`: `BigNumber` ; `fromPot?`: `BigNumber` ; `toPot?`: `BigNumber` ; `token?`: `string` }, [`MetadataType`](../enums/MetadataType.md)\> Move funds from one team to another @@ -347,11 +430,12 @@ import { Tokens, w } from '@colony/sdk'; // Immediately executing async function (async function() { // Move 10 of the native token from team 2 to team 3 + // (forced transaction example) await colony.moveFundsToTeam( w`10`, 2, 3, - ); + ).force(); })(); ``` @@ -366,9 +450,9 @@ import { Tokens, w } from '@colony/sdk'; #### Returns -`Promise`<[{ `agent?`: `string` ; `amount?`: `BigNumber` ; `fromPot?`: `BigNumber` ; `toPot?`: `BigNumber` ; `token?`: `string` }, `ContractReceipt`]\> +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)"``, { `agent?`: `string` ; `amount?`: `BigNumber` ; `fromPot?`: `BigNumber` ; `toPot?`: `BigNumber` ; `token?`: `string` }, [`MetadataType`](../enums/MetadataType.md)\> -A tupel of event data and contract receipt +A [TxCreator](TxCreator.md) **Event data** @@ -379,55 +463,3 @@ A tupel of event data and contract receipt | `toPot` | BigNumber | The target funding pot | | `amount` | BigNumber | The amount that was transferred | | `token` | string | The token address being transferred | - -___ - -### pay - -▸ **pay**(`recipient`, `amount`, `teamId?`, `tokenAddress?`): `Promise`<[{ `agent?`: `string` ; `fundamentalId?`: `BigNumber` ; `nPayouts?`: `BigNumber` }, `ContractReceipt`]\> - -Make a payment to a single address using a single token - -**`Remarks`** - -Requires the `OneTxPayment` extension to be installed for the Colony (this is usually the case for Colonies created via the Dapp). Note that most tokens use 18 decimals, so add a bunch of zeros or use our `w` or `toWei` functions (see example) - -**`Example`** - -```typescript -import { Id, Tokens, w } from '@colony/sdk'; - -// Immediately executing async function -(async function() { - // Pay 10 XDAI (on Gnosis chain) from the root domain to the following address - await colony.pay( - '0xb77D57F4959eAfA0339424b83FcFaf9c15407461', - w`10`, - Id.RootDomain, - Tokens.Gnosis.XDAI, - ); -})(); -``` - -#### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `recipient` | `string` | Wallet address of account to send the funds to (also awarded reputation when sending the native token) | -| `amount` | `BigNumberish` | Amount to pay in wei | -| `teamId?` | `BigNumberish` | The team to use to send the funds from. Has to have funding of at least the amount you need to send. See [Colony.moveFundsToTeam](Colony.md#movefundstoteam). Defaults to the Colony's root team | -| `tokenAddress?` | `string` | The address of the token to make the payment in. Default is the Colony's native token | - -#### Returns - -`Promise`<[{ `agent?`: `string` ; `fundamentalId?`: `BigNumber` ; `nPayouts?`: `BigNumber` }, `ContractReceipt`]\> - -A tupel of event data and contract receipt - -**Event data** - -| Property | Type | Description | -| :------ | :------ | :------ | -| `agent` | string | The address that is responsible for triggering this event | -| `fundamentalId` | BigNumber | The newly added payment id | -| `nPayouts` | BigNumber | Number of payouts in total | diff --git a/docs/api/classes/ColonyEventManager.md b/docs/api/classes/ColonyEventManager.md index 6bad1f9f..1783f8e7 100644 --- a/docs/api/classes/ColonyEventManager.md +++ b/docs/api/classes/ColonyEventManager.md @@ -18,7 +18,7 @@ Create a new ColonyEvents instance **`Remarks`** -As opposed to the ColonyNetwork.ColonyNetwork class, this constructor _needs_ an _ethers_ JsonRpcProvider (or a subclass of it) as it's +As opposed to the [ColonyNetwork](ColonyNetwork.md) class, this constructor _needs_ an _ethers_ JsonRpcProvider (or a subclass of it) as it's the only provider that supports topic filtering by multiple addresses #### Parameters @@ -64,7 +64,7 @@ We can do that as we do not have ambiguous events across our contracts, so we wi **`Example`** -Filter for all `DomainAdded` events between block 21830000 and 21840000 (across all deployed ColonyNetwork.Colony contracts) +Filter for all `DomainAdded` events between block 21830000 and 21840000 (across all deployed [ColonyNetwork](ColonyNetwork.md) contracts) ```typescript const domainAdded = colonyEvents.createFilter( colonyEvents.eventSources.Colony, @@ -116,7 +116,7 @@ We can do that as we do not have ambiguous events across our contracts, so we wi **`Example`** -Filter for all `DomainAdded` events for a specific ColonyNetwork.Colony contract +Filter for all `DomainAdded` events for a specific [Colony](Colony.md) contract ```typescript const domainAdded = colonyEvents.createFilter( colonyEvents.eventSources.Colony, @@ -150,7 +150,7 @@ ___ ### getEvents -▸ **getEvents**(`filter`): `Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<``"Annotation(address,bytes32,string)"`` \| ``"ColonyMetadata(address,string)"`` \| ``"DomainMetadata(address,uint256,string)"`` \| ``"Decision"`` \| ``"MISC"``\>[]\> +▸ **getEvents**<`T`\>(`filter`): `Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<`T`\>[]\> Get events for a single filter @@ -158,7 +158,7 @@ Gets events for an individual filter and automatically parses the data if possib **`Example`** -Retrieve and parse all `DomainAdded` events for a specific ColonyNetwork.Colony contract +Retrieve and parse all `DomainAdded` events for a specific [Colony](Colony.md) contract ```typescript const domainAdded = colonyEvents.createFilter( colonyEvents.eventSources.Colony, @@ -171,15 +171,21 @@ const domainAdded = colonyEvents.createFilter( })(); ``` +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends [`MetadataType`](../enums/MetadataType.md) | + #### Parameters | Name | Type | Description | | :------ | :------ | :------ | -| `filter` | [`ColonyFilter`](../interfaces/ColonyFilter.md) | A [ColonyFilter](../interfaces/ColonyFilter.md). ColonyMultiFilters will not work | +| `filter` | [`ColonyFilter`](../interfaces/ColonyFilter.md) | A [ColonyFilter](../interfaces/ColonyFilter.md). [ColonyMultiFilter](../interfaces/ColonyMultiFilter.md)s will not work | #### Returns -`Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<``"Annotation(address,bytes32,string)"`` \| ``"ColonyMetadata(address,string)"`` \| ``"DomainMetadata(address,uint256,string)"`` \| ``"Decision"`` \| ``"MISC"``\>[]\> +`Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<`T`\>[]\> An array of [ColonyEvent](../interfaces/ColonyEvent.md)s @@ -187,7 +193,7 @@ ___ ### getMultiEvents -▸ **getMultiEvents**(`filters`, `options?`): `Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<``"Annotation(address,bytes32,string)"`` \| ``"ColonyMetadata(address,string)"`` \| ``"DomainMetadata(address,uint256,string)"`` \| ``"Decision"`` \| ``"MISC"``\>[]\> +▸ **getMultiEvents**<`T`\>(`filters`, `options?`): `Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<`T`\>[]\> Get events for multiple filters across multiple addresses at once @@ -200,8 +206,8 @@ This is handy when you want to listen to a fixed set of events for a lot of diff **`Example`** -Retrieve and parse all `DomainAdded` and `DomainMetadata` events for a specific ColonyNetwork.Colony contract. -Note that we're using ColonyEvents.createMultiFilter here. The two `colonyAddress`es could also be different +Retrieve and parse all `DomainAdded` and `DomainMetadata` events for a specific [ColonyNetwork](ColonyNetwork.md) contract. +Note that we're using [createMultiFilter](ColonyEventManager.md#createmultifilter) here. The two `colonyAddress`es could also be different ```typescript const domainAdded = colonyEvents.createMultiFilter( @@ -221,17 +227,23 @@ const domainMetadata = colonyEvents.createMultiFilter( })(); ``` +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `T` | extends [`MetadataType`](../enums/MetadataType.md) | + #### Parameters | Name | Type | Description | | :------ | :------ | :------ | -| `filters` | [`ColonyMultiFilter`](../interfaces/ColonyMultiFilter.md)[] | An array of [ColonyMultiFilter](../interfaces/ColonyMultiFilter.md)s. Normal ColonyFilters will not work | +| `filters` | [`ColonyMultiFilter`](../interfaces/ColonyMultiFilter.md)[] | An array of [ColonyMultiFilter](../interfaces/ColonyMultiFilter.md)s. Normal [ColonyFilter](../interfaces/ColonyFilter.md)s will not work | | `options` | `Object` | You can define `fromBlock` and `toBlock` only once for all the filters given | | `options.fromBlock?` | `BlockTag` | Starting block in which to look for this event - inclusive (default: 'latest') | | `options.toBlock?` | `BlockTag` | Ending block in which to look for this event - inclusive (default: 'latest') | #### Returns -`Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<``"Annotation(address,bytes32,string)"`` \| ``"ColonyMetadata(address,string)"`` \| ``"DomainMetadata(address,uint256,string)"`` \| ``"Decision"`` \| ``"MISC"``\>[]\> +`Promise`<[`ColonyEvent`](../interfaces/ColonyEvent.md)<`T`\>[]\> An array of [ColonyEvent](../interfaces/ColonyEvent.md)s diff --git a/docs/api/classes/ColonyNetwork.md b/docs/api/classes/ColonyNetwork.md index 678ce35b..b271619c 100644 --- a/docs/api/classes/ColonyNetwork.md +++ b/docs/api/classes/ColonyNetwork.md @@ -28,16 +28,28 @@ const colonyNetwork = new ColonyNetwork(provider); | Name | Type | Description | | :------ | :------ | :------ | | `signerOrProvider` | `SignerOrProvider` | An _ethers_ compatible Signer or Provider instance | -| `options?` | `ColonyNetworkOptions` | Optional custom ColonyNetworkOptions | +| `options?` | [`ColonyNetworkOptions`](../interfaces/ColonyNetworkOptions.md) | Optional custom [ColonyNetworkOptions](../interfaces/ColonyNetworkOptions.md) | ## Properties +### config + +• **config**: `ColonyNetworkConfig` + +___ + ### ipfs • **ipfs**: `IpfsMetadata` ___ +### network + +• **network**: [`Network`](../enums/Network.md) + +___ + ### networkClient • **networkClient**: `ColonyNetworkClient` diff --git a/docs/api/classes/ColonyToken.md b/docs/api/classes/ColonyToken.md index 7d815170..64ff0d2e 100644 --- a/docs/api/classes/ColonyToken.md +++ b/docs/api/classes/ColonyToken.md @@ -1,5 +1,11 @@ # Class: ColonyToken +## Properties + +### tokenLockingClient + +• **tokenLockingClient**: `TokenLockingClient` + ## Methods ### approve @@ -15,11 +21,14 @@ In order for the wallet owner to stake tokens, that amount has to be approved an ```typescript import { w } from '@colony/sdk'; -const token = colony.getToken(); -// Approve 100 tokens to be "activated" -await token.approve(w`100`); -// Deposit the tokens -await token.deposit(w`100`); +// Immediately executing async function +(async function() { + const token = await colony.getToken(); + // Approve 100 tokens to be "activated" + await token.approve(w`100`); + // Deposit the tokens + await token.deposit(w`100`); +})(); ``` #### Parameters @@ -57,11 +66,14 @@ In order for the wallet owner to stake tokens, that amount has to be approved an ```typescript import { w } from '@colony/sdk'; -const token = colony.getToken(); -// Approve 100 tokens to be "activated" -await token.approve(w`100`); -// Deposit the tokens -await token.deposit(w`100`); +// Immediately executing async function +(async function() { + const token = await colony.getToken(); + // Approve 100 tokens to be "activated" + await token.approve(w`100`); + // Deposit the tokens + await token.deposit(w`100`); +})(); ``` #### Parameters @@ -86,16 +98,6 @@ A tupel of event data and contract receipt ___ -### getTokenLockingClient - -▸ **getTokenLockingClient**(): `Promise`<`TokenLockingClient`\> - -#### Returns - -`Promise`<`TokenLockingClient`\> - -___ - ### getUserApproval ▸ **getUserApproval**(`user`, `obligator`): `Promise`<`BigNumber`\> @@ -143,7 +145,7 @@ ___ ### mint -▸ **mint**(`amount`): `Promise`<[{}, `ContractReceipt`]\> +▸ **mint**(`amount`): [`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"mintTokens"``, `Record`<`string`, `unknown`\>, [`MetadataType`](../enums/MetadataType.md)\> Mints `amount` of a Colony's native token. @@ -156,11 +158,16 @@ Only works for tokens deployed with Colony (not imported tokens). Note that most ```typescript import { w } from '@colony/sdk'; -const token = colony.getToken(); -// Mint 100 tokens of the Colony's native token -await token.mint(w`100`); -// Claim the minted tokens for the Colony -await colony.claimFunds(); +// Immediately executing async function +(async function() { + const token = await colony.getToken(); + // Mint 100 tokens of the Colony's native token + // (forced transaction example) + await token.mint(w`100`).force(); + // Claim the minted tokens for the Colony + // (forced transaction example) + await colony.claimFunds().force(); +})(); ``` #### Parameters @@ -171,9 +178,9 @@ await colony.claimFunds(); #### Returns -`Promise`<[{}, `ContractReceipt`]\> +[`TxCreator`](TxCreator.md)<`ColonyClientV10`, ``"mintTokens"``, `Record`<`string`, `unknown`\>, [`MetadataType`](../enums/MetadataType.md)\> -A tupel of event data and contract receipt +A [TxCreator](TxCreator.md) ___ @@ -204,9 +211,12 @@ Does the opposite of `deposit` and frees the deposited tokens back to the wallet ```typescript import { w } from '@colony/sdk'; -const token = colony.getToken(); -// Withdraw 100 tokens that were previously deposited -await token.withdraw(w`100`); +// Immediately executing async function +(async function() { + const token = await colony.getToken(); + // Withdraw 100 tokens that were previously deposited + await token.withdraw(w`100`); +})(); ``` #### Parameters diff --git a/docs/api/classes/MotionCreator.md b/docs/api/classes/MotionCreator.md deleted file mode 100644 index 82149b8b..00000000 --- a/docs/api/classes/MotionCreator.md +++ /dev/null @@ -1,214 +0,0 @@ -# Class: MotionCreator - -MotionCreator - -This is part of the [VotingReputation](VotingReputation.md) class and not to be meant to instantiated directly. -You can find an instance of this under `colony.ext.motions.create` - -## Constructors - -### constructor - -• **new MotionCreator**(`colony`, `votingReputationClient`) - -#### Parameters - -| Name | Type | -| :------ | :------ | -| `colony` | [`Colony`](Colony.md) | -| `votingReputationClient` | `VotingReputationClientV7` | - -## Methods - -### createTeam - -▸ **createTeam**(`metadataCid?`): `Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -Create a motion to create a team within a Colony - -For more information about the resulting action, see [Colony.createTeam](Colony.md#createteam). - -**`Remarks`** - -Currently you can only add domains within the `Root` domain. This restriction will be lifted soon - -#### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `metadataCid?` | `string` | An IPFS [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file containing the metadata described below. For now, we would like to keep it agnostic to any IPFS upload mechanism, so you have to upload the file manually and provide your own hash (by using, for example, [Pinata](https://docs.pinata.cloud/)) | - -#### Returns - -`Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -A tupel: `[eventData, ContractReceipt]` - -**Motion event data** - -| Property | Type | Description | -| :------ | :------ | :------ | -| `motionId` | BigNumber | ID of the motion created | -| `creator` | string | Address of the motion's creator | -| `domainId` | BigNumber | Team ID of the motion | - -___ - -### makeArbitraryTransaction - -▸ **makeArbitraryTransaction**(`target`, `action`): `Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -Create a motion to create an arbitrary transaction in the name of the Colony - -For more information about the resulting action, see [Colony.makeArbitraryTransaction](Colony.md#makearbitrarytransaction). - -This method can execute a transaction on any Ethereum Smart Contract with the Colony address as the sender. The action needs to be encoded function data (see https://docs.ethers.io/v5/api/utils/abi/interface/#Interface--encoding). We provide some common interfaces for you to make it even easier. - -**`Example`** - -Create a motion to mint an NFT from a Colony -```typescript -import { ERC721 } from '@colony/sdk'; - -// Mint a NFT for address 0xb794f5ea0ba39494ce839613fffba74279579268 -const encodedAction = ERC721.encodeFunctionData( - 'mintTo', - '0xb794f5ea0ba39494ce839613fffba74279579268', -); - -// Immediately executing async function -(async function() { - await colony.ext.motions.create.makeArbitraryTransaction( - // NFT contract address - '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d', - // encoded transaction from above - encodedAction - ); -})(); -``` - -#### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `target` | `string` | Address of the contract to execute a method on | -| `action` | `BytesLike` | Encoded action to execute | - -#### Returns - -`Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -A tupel of event data and contract receipt - -**Motion event data** - -| Property | Type | Description | -| :------ | :------ | :------ | -| `motionId` | BigNumber | ID of the motion created | -| `creator` | string | Address of the motion's creator | -| `domainId` | BigNumber | Team ID of the motion | - -___ - -### moveFundsToTeam - -▸ **moveFundsToTeam**(`amount`, `toTeam`, `fromTeam?`, `tokenAddress?`): `Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -Create a motion to move funds from one team to another - -For more information about the resulting action, see [Colony.moveFundsToTeam](Colony.md#movefundstoteam). - -After sending funds to and claiming funds for your Colony they will land in a special team, the root team. If you want to make payments from other teams (in order to award reputation in that team) you have to move the funds there first. Use this method to do so. - -**`Example`** - -```typescript -import { Tokens } from '@colony/sdk'; - -// Immediately executing async function -(async function() { - // Move 10 of the native token from team 2 to team 3 - await colony.ext.motions.create.moveFundsToTeam( - w`10`, - 2, - 3, - ); -})(); -``` - -#### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `amount` | `BigNumberish` | Amount to transfer between the teams | -| `toTeam` | `BigNumberish` | The team to transfer the funds to | -| `fromTeam?` | `BigNumberish` | The team to transfer the funds from. Default is the Root team | -| `tokenAddress?` | `string` | The address of the token to be transferred. Default is the Colony's native token | - -#### Returns - -`Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -A tupel of event data and contract receipt - -**Motion event data** - -| Property | Type | Description | -| :------ | :------ | :------ | -| `motionId` | BigNumber | ID of the motion created | -| `creator` | string | Address of the motion's creator | -| `domainId` | BigNumber | Team ID of the motion | - -___ - -### pay - -▸ **pay**(`recipient`, `amount`, `teamId?`, `tokenAddress?`): `Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -Create a motion to make a payment to a single address using a single token - -For more information about the resulting action, see [Colony.pay](Colony.md#pay). - -**`Remarks`** - -Requires the `OneTxPayment` extension to be installed for the Colony (this is usually the case for Colonies created via the Dapp). Note that most tokens use 18 decimals, so add a bunch of zeros or use our `w` or `toWei` functions (see example) - -**`Example`** - -```typescript -import { Id, Tokens, w } from '@colony/sdk'; - -// Immediately executing async function -(async function() { - // Pay 10 XDAI (on Gnosis chain) from the root domain to the following address - await colony.ext.motions.create.pay( - '0xb77D57F4959eAfA0339424b83FcFaf9c15407461', - w`10`, - Id.RootDomain, - Tokens.Gnosis.XDAI, - ); -})(); -``` - -#### Parameters - -| Name | Type | Description | -| :------ | :------ | :------ | -| `recipient` | `string` | Wallet address of account to send the funds to (also awarded reputation when sending the native token) | -| `amount` | `BigNumberish` | Amount to pay in wei | -| `teamId?` | `BigNumberish` | The team to use to send the funds from. Has to have funding of at least the amount you need to send. See [Colony.moveFundsToTeam](Colony.md#movefundstoteam). Defaults to the Colony's root team | -| `tokenAddress?` | `string` | The address of the token to make the payment in. Default is the Colony's native token | - -#### Returns - -`Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> - -A tupel of event data and contract receipt - -**Motion event data** - -| Property | Type | Description | -| :------ | :------ | :------ | -| `motionId` | BigNumber | ID of the motion created | -| `creator` | string | Address of the motion's creator | -| `domainId` | BigNumber | Team ID of the motion | diff --git a/docs/api/classes/PinataAdapter.md b/docs/api/classes/PinataAdapter.md index 4cb7a09c..16e455c2 100644 --- a/docs/api/classes/PinataAdapter.md +++ b/docs/api/classes/PinataAdapter.md @@ -2,7 +2,7 @@ PinataAdapter -A Colony SDK IPFS adapter for Pinata (https://pinata.cloud). In order to use this, sign up for Pinata (if you haven't already) and generate a token. Then either supply this token when instantiating the class (example below) or provide it via the environment variable `COLONY_IPFS_PINATA_TOKEN`. Then provide an instance of this class to the [ColonyNetwork](ColonyNetwork.md) or ColonyEventManger classes (depending on your needs). +A Colony SDK IPFS adapter for Pinata (https://pinata.cloud). In order to use this, sign up for Pinata (if you haven't already) and generate a token. Then either supply this token when instantiating the class (example below) or provide it via the environment variable `COLONY_IPFS_PINATA_TOKEN` (when using NodeJS). Then provide an instance of this class to the [ColonyNetwork](ColonyNetwork.md) or [ColonyEventManager](ColonyEventManager.md) classes (depending on your needs). **`Remarks`** diff --git a/docs/api/classes/TxCreator.md b/docs/api/classes/TxCreator.md new file mode 100644 index 00000000..3611f810 --- /dev/null +++ b/docs/api/classes/TxCreator.md @@ -0,0 +1,142 @@ +# Class: TxCreator + +An umbrella API for all kinds of transactions + +The `TxCreator` allows for a simple API to cover all the different cases of transactions within Colony. Once a `TxCreator` is created using a method on the base contracts (e.g. [Colony](Colony.md) or extensions like [VotingReputation](VotingReputation.md)), there are four options available: + +## Force a transaction + +- [TxCreator.force](TxCreator.md#force): force a Colony transaction, knowing you have the permissions to do so +- [TxCreator.forceMeta](TxCreator.md#forcemeta): same as `force()`, but send as a gasless metatransaction + +## Create a motion to trigger an action once it passes + +- [TxCreator.motion](TxCreator.md#motion): create a motion (needs the motion's domain as a parameter) +- [TxCreator.forceMeta](TxCreator.md#forcemeta): same as `motion()`, but sends a gasless metatransaction + +Learn more about these functions in their individual documentation + +## Type parameters + +| Name | Type | +| :------ | :------ | +| `C` | extends `BaseContract` | +| `M` | extends keyof `C`[``"functions"``] | +| `E` | extends `EventData` | +| `MD` | extends [`MetadataType`](../enums/MetadataType.md) | + +## Constructors + +### constructor + +• **new TxCreator**<`C`, `M`, `E`, `MD`\>(`__namedParameters`) + +#### Type parameters + +| Name | Type | +| :------ | :------ | +| `C` | extends `BaseContract` | +| `M` | extends `string` \| `number` \| `symbol` | +| `E` | extends `EventData` | +| `MD` | extends [`MetadataType`](../enums/MetadataType.md) | + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `__namedParameters` | `Object` | +| `__namedParameters.args` | `unknown`[] \| () => `Promise`<`unknown`[]\> | +| `__namedParameters.colony` | [`Colony`](Colony.md) | +| `__namedParameters.contract` | `C` | +| `__namedParameters.eventData?` | (`receipt`: `ContractReceipt`) => `Promise`<`E`\> | +| `__namedParameters.metadataType?` | `MD` | +| `__namedParameters.method` | `M` | +| `__namedParameters.permissionConfig?` | `PermissionConfig` | + +## Methods + +### force + +▸ **force**(): `Promise`<[`E`, `ContractReceipt`, () => `Promise`<`ReturnType`<{ `None`: () => `void` ; `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[`MD`]\>\>] \| [`E`, `ContractReceipt`]\> + +Forces an action + +**`Remarks`** + +The user sending this transaction has to have the appropriate permissions to do so. Learn more about permissions in Colony [here](/develop/dev-learning/permissions). + +#### Returns + +`Promise`<[`E`, `ContractReceipt`, () => `Promise`<`ReturnType`<{ `None`: () => `void` ; `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[`MD`]\>\>] \| [`E`, `ContractReceipt`]\> + +A tupel of event data and contract receipt (and a function to retrieve metadata if applicable) + +___ + +### forceMeta + +▸ **forceMeta**(): `Promise`<[`E`, [`ParsedLogTransactionReceipt`](../interfaces/ParsedLogTransactionReceipt.md), () => `Promise`<`ReturnType`<{ `None`: () => `void` ; `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[`MD`]\>\>] \| [`E`, [`ParsedLogTransactionReceipt`](../interfaces/ParsedLogTransactionReceipt.md)]\> + +Forces an action using a gasless metatransaction + +**`Remarks`** + +The user sending this transaction has to have the appropriate permissions to do so. Learn more about permissions in Colony [here](/develop/dev-learning/permissions). + +#### Returns + +`Promise`<[`E`, [`ParsedLogTransactionReceipt`](../interfaces/ParsedLogTransactionReceipt.md), () => `Promise`<`ReturnType`<{ `None`: () => `void` ; `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[`MD`]\>\>] \| [`E`, [`ParsedLogTransactionReceipt`](../interfaces/ParsedLogTransactionReceipt.md)]\> + +A tupel of event data and contract receipt (and a function to retrieve metadata if applicable) + +___ + +### motion + +▸ **motion**(`motionDomain?`): `Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> + +Creates a motion for an action + +You can specify a team (domain) this motion should be created in. It will be created in the Root team by default. + +**`Remarks`** + +This will only work if the [VotingReputation](VotingReputation.md) extension is installed for the Colony that's being acted on + +#### Parameters + +| Name | Type | Default value | +| :------ | :------ | :------ | +| `motionDomain` | `BigNumberish` | `Id.RootDomain` | + +#### Returns + +`Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> + +A tupel of motion event data and contract receipt + +___ + +### motionMeta + +▸ **motionMeta**(`motionDomain?`): `Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, [`ParsedLogTransactionReceipt`](../interfaces/ParsedLogTransactionReceipt.md)]\> + +Creates a motion for an action + +You can specify a team (domain) this motion should be created in. It will be created in the Root team by default. + +**`Remarks`** + +This will only work if the [VotingReputation](VotingReputation.md) extension is installed for the Colony that's being acted on + +#### Parameters + +| Name | Type | Default value | +| :------ | :------ | :------ | +| `motionDomain` | `BigNumberish` | `Id.RootDomain` | + +#### Returns + +`Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, [`ParsedLogTransactionReceipt`](../interfaces/ParsedLogTransactionReceipt.md)]\> + +A tupel of motion event data and contract receipt diff --git a/docs/api/classes/VotingReputation.md b/docs/api/classes/VotingReputation.md index d5a32311..23a85c0f 100644 --- a/docs/api/classes/VotingReputation.md +++ b/docs/api/classes/VotingReputation.md @@ -19,9 +19,9 @@ The exact lifecycle is determined by the parameters that were set when the `Voti #### Creating a Motion -See [MotionCreator](MotionCreator.md). +See [TxCreator.motion](TxCreator.md#motion) or [TxCreator.motionMeta](TxCreator.md#motionmeta). -Anyone within a Colony can start a motion. In Colony SDK, this can be done with the [MotionCreator](MotionCreator.md) API. There the `action` (the contract transaction) for the Motion will be defined. This is essentially nothing else than an encoded contract function string alongside its parameters (see for detailed info [here](https://medium.com/linum-labs/a-technical-primer-on-using-encoded-function-calls-50e2b9939223) - but don't worry. In Colony SDK this will all be taken care of by the [MotionCreator](MotionCreator.md)). +Anyone within a Colony can start a motion. In Colony SDK, this is as easy as sending a transaction of the same kind. There the `action` (the contract transaction) for the Motion will be defined. This is essentially nothing else than an encoded contract function string alongside its parameters (see for detailed info [here](https://medium.com/linum-labs/a-technical-primer-on-using-encoded-function-calls-50e2b9939223) - but don't worry. In Colony SDK this will all be taken care of by the [TxCreator](TxCreator.md)). #### Staking @@ -102,12 +102,6 @@ You can - at any point in the lifecycle inspect the current state of a Motion. U • **address**: `string` -___ - -### create - -• **create**: [`MotionCreator`](MotionCreator.md) - ## Methods ### approveStake @@ -141,6 +135,32 @@ A tupel of event data and contract receipt ___ +### createMotion + +▸ **createMotion**(`motionDomain`, `encodedAction`, `altTarget?`): `Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> + +Create a motion using an encoded action + +**`Remarks`** + +You will usually not use this function directly, but use the `send` or `motion` functions of the [TxCreator](TxCreator.md) within the relevant contract. + +#### Parameters + +| Name | Type | Default value | Description | +| :------ | :------ | :------ | :------ | +| `motionDomain` | `BigNumberish` | `undefined` | The domain the motion will be created in | +| `encodedAction` | `string` | `undefined` | The encoded action as a string | +| `altTarget` | `string` | `'0x0'` | The contract to which we send the action - 0x0 for the colony (default) | + +#### Returns + +`Promise`<[{ `creator?`: `string` ; `domainId?`: `BigNumber` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> + +A Motion object + +___ + ### finalizeMotion ▸ **finalizeMotion**(`motionId`): `Promise`<[{ `action?`: `string` ; `executed?`: `boolean` ; `motionId?`: `BigNumber` }, `ContractReceipt`]\> @@ -185,7 +205,7 @@ Get the minimum stake that has to be supplied for a motion and a certain vote (N **`Remarks`** -To get the missing amount for activation, call getMotionStakes +To get the missing amount for activation, call [getRemainingStakes](VotingReputation.md#getremainingstakes) #### Parameters diff --git a/docs/api/enums/MetaTxBroadCasterEndpoint.md b/docs/api/enums/MetaTxBroadCasterEndpoint.md new file mode 100644 index 00000000..98985d83 --- /dev/null +++ b/docs/api/enums/MetaTxBroadCasterEndpoint.md @@ -0,0 +1,49 @@ +# Enumeration: MetaTxBroadCasterEndpoint + +## Enumeration Members + +### Custom + +• **Custom** = ``""`` + +The metatransaction broadcaster endpoint for a custom network + +___ + +### Gnosis + +• **Gnosis** = ``"https://xdai.colony.io/metatransaction/xdai"`` + +The metatransaction broadcaster endpointon Gnosis chain + +___ + +### Goerli + +• **Goerli** = ``""`` + +The metatransaction broadcaster endpoint on the Görli testnet + +___ + +### Mainnet + +• **Mainnet** = ``""`` + +The metatransaction broadcaster endpoint on mainnet + +___ + +### Xdai + +• **Xdai** = ``"https://xdai.colony.io/metatransaction/xdai"`` + +The metatransaction broadcaster endpointon Gnosis chain (alias) + +___ + +### XdaiQa + +• **XdaiQa** = ``"https://xdai.colony.io/metatransaction/xdai"`` + +The metatransaction broadcaster endpointaddress on Gnosis chain diff --git a/docs/api/enums/MetadataType.md b/docs/api/enums/MetadataType.md new file mode 100644 index 00000000..91821896 --- /dev/null +++ b/docs/api/enums/MetadataType.md @@ -0,0 +1,31 @@ +# Enumeration: MetadataType + +## Enumeration Members + +### Annotation + +• **Annotation** = ``"annotation"`` + +___ + +### Colony + +• **Colony** = ``"colony"`` + +___ + +### Decision + +• **Decision** = ``"decision"`` + +___ + +### Domain + +• **Domain** = ``"domain"`` + +___ + +### Misc + +• **Misc** = ``"misc"`` diff --git a/docs/api/enums/Network.md b/docs/api/enums/Network.md new file mode 100644 index 00000000..f92b75b4 --- /dev/null +++ b/docs/api/enums/Network.md @@ -0,0 +1,51 @@ +# Enumeration: Network + +Supported Ethereum networks. Use `Custom` if you'd like to bring your own deployment (e.g. local) + +## Enumeration Members + +### Custom + +• **Custom** = ``"Custom"`` + +Use this to specify an own main ColonyNetwork address in the options + +___ + +### Gnosis + +• **Gnosis** = ``"Xdai"`` + +Gnosis chain + +___ + +### Goerli + +• **Goerli** = ``"Goerli"`` + +Goerli testnet + +___ + +### Mainnet + +• **Mainnet** = ``"Mainnet"`` + +Ethereum Mainnet + +___ + +### Xdai + +• **Xdai** = ``"Xdai"`` + +Gnosis chain (alias) + +___ + +### XdaiQa + +• **XdaiQa** = ``"XdaiQa"`` + +Gnosis chain custom fork diff --git a/docs/api/enums/TeamColor.md b/docs/api/enums/TeamColor.md new file mode 100644 index 00000000..53895fa8 --- /dev/null +++ b/docs/api/enums/TeamColor.md @@ -0,0 +1,97 @@ +# Enumeration: TeamColor + +## Enumeration Members + +### Aqua + +• **Aqua** = ``10`` + +___ + +### Black + +• **Black** = ``2`` + +___ + +### Blue + +• **Blue** = ``4`` + +___ + +### BlueGrey + +• **BlueGrey** = ``11`` + +___ + +### EmeraldGreen + +• **EmeraldGreen** = ``3`` + +___ + +### Gold + +• **Gold** = ``9`` + +___ + +### Green + +• **Green** = ``7`` + +___ + +### LightPink + +• **LightPink** = ``0`` + +___ + +### Magenta + +• **Magenta** = ``14`` + +___ + +### Orange + +• **Orange** = ``13`` + +___ + +### Periwinkle + +• **Periwinkle** = ``8`` + +___ + +### Pink + +• **Pink** = ``1`` + +___ + +### Purple + +• **Purple** = ``12`` + +___ + +### PurpleGrey + +• **PurpleGrey** = ``15`` + +___ + +### Red + +• **Red** = ``6`` + +___ + +### Yellow + +• **Yellow** = ``5`` diff --git a/docs/api/interfaces/AnnotationMetadata.md b/docs/api/interfaces/AnnotationMetadata.md new file mode 100644 index 00000000..3fb47c36 --- /dev/null +++ b/docs/api/interfaces/AnnotationMetadata.md @@ -0,0 +1,7 @@ +# Interface: AnnotationMetadata + +## Properties + +### annotationMsg + +• **annotationMsg**: `string` diff --git a/docs/api/interfaces/ColonyEvent.md b/docs/api/interfaces/ColonyEvent.md index 92b3efd8..69a1e0a6 100644 --- a/docs/api/interfaces/ColonyEvent.md +++ b/docs/api/interfaces/ColonyEvent.md @@ -1,4 +1,4 @@ -# Interface: ColonyEvent +# Interface: ColonyEvent An Event that came from a contract within the Colony Network @@ -6,7 +6,7 @@ An Event that came from a contract within the Colony Network | Name | Type | | :------ | :------ | -| `E` | extends [`MetadataEvent`](../README.md#metadataevent) = [`MetadataEvent`](../README.md#metadataevent) | +| `T` | extends [`MetadataType`](../enums/MetadataType.md) | ## Hierarchy @@ -68,15 +68,15 @@ ___ ### getMetadata -• `Optional` **getMetadata**: () => `Promise`<`ReturnType`<{ `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[{ `Annotation(address,bytes32,string)`: `Annotation` = MetadataType.Annotation; `ColonyMetadata(address,string)`: `Colony` = MetadataType.Colony; `Decision`: `Decision` = MetadataType.Decision; `DomainMetadata(address,uint256,string)`: `Domain` = MetadataType.Domain; `MISC`: `Misc` = MetadataType.Misc }[`E`]]\>\> +• `Optional` **getMetadata**: () => `Promise`<`ReturnType`<{ `None`: () => `void` ; `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[`T`]\>\> #### Type declaration -▸ (): `Promise`<`ReturnType`<{ `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[{ `Annotation(address,bytes32,string)`: `Annotation` = MetadataType.Annotation; `ColonyMetadata(address,string)`: `Colony` = MetadataType.Colony; `Decision`: `Decision` = MetadataType.Decision; `DomainMetadata(address,uint256,string)`: `Domain` = MetadataType.Domain; `MISC`: `Misc` = MetadataType.Misc }[`E`]]\>\> +▸ (): `Promise`<`ReturnType`<{ `None`: () => `void` ; `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[`T`]\>\> ##### Returns -`Promise`<`ReturnType`<{ `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[{ `Annotation(address,bytes32,string)`: `Annotation` = MetadataType.Annotation; `ColonyMetadata(address,string)`: `Colony` = MetadataType.Colony; `Decision`: `Decision` = MetadataType.Decision; `DomainMetadata(address,uint256,string)`: `Domain` = MetadataType.Domain; `MISC`: `Misc` = MetadataType.Misc }[`E`]]\>\> +`Promise`<`ReturnType`<{ `None`: () => `void` ; `annotation`: (`res`: `string`) => `undefined` \| `string` = getAnnotationMsgFromResponse; `colony`: (`res`: `string`) => `undefined` \| `ColonyMetadata` = getColonyMetadataFromResponse; `decision`: (`res`: `string`) => `undefined` \| `DecisionMetadata` = getDecisionDetailsFromResponse; `domain`: (`res`: `string`) => `undefined` \| `DomainMetadata` = getDomainMetadataFromResponse; `misc`: (`res`: `string`) => `undefined` \| `MiscMetadata` = getMiscDataFromResponse }[`T`]\>\> ___ diff --git a/docs/api/interfaces/ColonyMultiFilter.md b/docs/api/interfaces/ColonyMultiFilter.md index 759f4780..6c9f1668 100644 --- a/docs/api/interfaces/ColonyMultiFilter.md +++ b/docs/api/interfaces/ColonyMultiFilter.md @@ -16,4 +16,4 @@ ___ ### colonyTopics -• **colonyTopics**: `ColonyTopic`[] +• **colonyTopics**: [`ColonyTopic`](ColonyTopic.md)[] diff --git a/docs/api/interfaces/ColonyNetworkOptions.md b/docs/api/interfaces/ColonyNetworkOptions.md new file mode 100644 index 00000000..027ef46f --- /dev/null +++ b/docs/api/interfaces/ColonyNetworkOptions.md @@ -0,0 +1,27 @@ +# Interface: ColonyNetworkOptions + +Additional options for the [ColonyNetwork](../classes/ColonyNetwork.md) + +## Properties + +### ipfsAdapter + +• `Optional` **ipfsAdapter**: [`IpfsAdapter`](IpfsAdapter.md) + +Provide a custom [IpfsAdapter](IpfsAdapter.md) + +___ + +### metaTxBroadcasterEndpoint + +• `Optional` **metaTxBroadcasterEndpoint**: `string` + +Provide a custom metatransaction broadcaster endpoint + +___ + +### networkClientOptions + +• `Optional` **networkClientOptions**: [`NetworkClientOptions`](NetworkClientOptions.md) + +Provide custom [NetworkClientOptions](NetworkClientOptions.md) for the ColonyJS client diff --git a/docs/api/interfaces/ColonyTopic.md b/docs/api/interfaces/ColonyTopic.md new file mode 100644 index 00000000..1843e855 --- /dev/null +++ b/docs/api/interfaces/ColonyTopic.md @@ -0,0 +1,21 @@ +# Interface: ColonyTopic + +A Colony specific topic that keeps track of which contract it belongs to + +## Properties + +### eventName + +• **eventName**: `string` + +___ + +### eventSource + +• **eventSource**: keyof [`EventSources`](EventSources.md) + +___ + +### topic + +• **topic**: `string` diff --git a/docs/api/interfaces/NetworkClientOptions.md b/docs/api/interfaces/NetworkClientOptions.md new file mode 100644 index 00000000..247a5b18 --- /dev/null +++ b/docs/api/interfaces/NetworkClientOptions.md @@ -0,0 +1,13 @@ +# Interface: NetworkClientOptions + +## Properties + +### networkAddress + +• `Optional` **networkAddress**: `string` + +___ + +### reputationOracleEndpoint + +• `Optional` **reputationOracleEndpoint**: `string` diff --git a/docs/api/interfaces/ParsedLogTransactionReceipt.md b/docs/api/interfaces/ParsedLogTransactionReceipt.md new file mode 100644 index 00000000..44d2aede --- /dev/null +++ b/docs/api/interfaces/ParsedLogTransactionReceipt.md @@ -0,0 +1,185 @@ +# Interface: ParsedLogTransactionReceipt + +Custom Transaction receipt for when we manually have to parse logs (metatransactions) + +## Hierarchy + +- `TransactionReceipt` + + ↳ **`ParsedLogTransactionReceipt`** + +## Properties + +### blockHash + +• **blockHash**: `string` + +#### Inherited from + +TransactionReceipt.blockHash + +___ + +### blockNumber + +• **blockNumber**: `number` + +#### Inherited from + +TransactionReceipt.blockNumber + +___ + +### byzantium + +• **byzantium**: `boolean` + +#### Inherited from + +TransactionReceipt.byzantium + +___ + +### confirmations + +• **confirmations**: `number` + +#### Inherited from + +TransactionReceipt.confirmations + +___ + +### contractAddress + +• **contractAddress**: `string` + +#### Inherited from + +TransactionReceipt.contractAddress + +___ + +### cumulativeGasUsed + +• **cumulativeGasUsed**: `BigNumber` + +#### Inherited from + +TransactionReceipt.cumulativeGasUsed + +___ + +### effectiveGasPrice + +• **effectiveGasPrice**: `BigNumber` + +#### Inherited from + +TransactionReceipt.effectiveGasPrice + +___ + +### from + +• **from**: `string` + +#### Inherited from + +TransactionReceipt.from + +___ + +### gasUsed + +• **gasUsed**: `BigNumber` + +#### Inherited from + +TransactionReceipt.gasUsed + +___ + +### logs + +• **logs**: `Log`[] + +#### Inherited from + +TransactionReceipt.logs + +___ + +### logsBloom + +• **logsBloom**: `string` + +#### Inherited from + +TransactionReceipt.logsBloom + +___ + +### parsedLogs + +• **parsedLogs**: `LogDescription`[] + +___ + +### root + +• `Optional` **root**: `string` + +#### Inherited from + +TransactionReceipt.root + +___ + +### status + +• `Optional` **status**: `number` + +#### Inherited from + +TransactionReceipt.status + +___ + +### to + +• **to**: `string` + +#### Inherited from + +TransactionReceipt.to + +___ + +### transactionHash + +• **transactionHash**: `string` + +#### Inherited from + +TransactionReceipt.transactionHash + +___ + +### transactionIndex + +• **transactionIndex**: `number` + +#### Inherited from + +TransactionReceipt.transactionIndex + +___ + +### type + +• **type**: `number` + +#### Inherited from + +TransactionReceipt.type diff --git a/docs/api/interfaces/SupportedExtensions.md b/docs/api/interfaces/SupportedExtensions.md new file mode 100644 index 00000000..16128fb9 --- /dev/null +++ b/docs/api/interfaces/SupportedExtensions.md @@ -0,0 +1,13 @@ +# Interface: SupportedExtensions + +## Properties + +### motions + +• `Optional` **motions**: [`VotingReputation`](../classes/VotingReputation.md) + +___ + +### oneTx + +• `Optional` **oneTx**: `OneTxPayment` diff --git a/docs/guides/transactions.md b/docs/guides/transactions.md new file mode 100644 index 00000000..bc21402d --- /dev/null +++ b/docs/guides/transactions.md @@ -0,0 +1,66 @@ +# How to create transactions with Colony SDK + +Within Colony, there are a few ways to do an action. As a colony is a permissioned contract, not everyone can just do anything they like, users (or contracts) have to have the right permission in the relevant team to do so. +If a governance extension is installed for the colony, this behaviour changes. Using [Motions & Disputes](../api/classes/VotingReputation.md) for example, it is possible to propose an action without having the appropriate permissions. +As permissioned functions and governance functions take the same arguments, we have unified this in one API, that could be extended in the future. + +## Creating transactions and motions + +So what does this mean? Let's look at an example. We would like to create a team using Colony SDK. If we have the right permissions, we can just do: + +```typescript +// Immediately executing async function +(async function() { + // Create a new team (domain) within our colony (using sheer force ;) ) + const [{ domainId }] = await colony.createTeam().force(); +})(); +``` + +**Note the `force()`** at the end.** That's where we tell Colony SDK to create a transaction that will take its action immediately, given we have the right permissions. + +If we wanted to create a motion instead (see [[VotingReputation]]) to create a new team, we'd replace `force()` with `motion(motionTeam)`: + +```typescript +import { Id } from '@colony/sdk'; +// Immediately executing async function +(async function() { + // Create a motion in the Root team to create a new team. Will have to go through the whole motion workflow + const [{ motionId }] = await colony.createTeam().motion(Id.RootDomain); +})(); +``` + +Note that you have to supply a `motionTeam` when creating a motion. This is the id of the team in which the motion will be created. This will have an effect on who will be able to object or vote and with how much reputation. + +**If the `motionTeam` is not specified it will default to the Root domain** + + +## Creating gasless transactions and motions (MetaTransactions) + +Colony SDK supports another way of sending off transactions or motions which we call *MetaTransactions*. These are gasless transactions (which makes them entirely free for the user) and are signed by the user who wants to issue them and send off by a Colony server. To send a MetaTransaction, just use `forceMeta()` instead of `force()` and `motionMeta()` instead of `motion()`. The wallet then needs to sign a message instead of a transaction, which will be transferred to the Colony MetaTransaction broadcaster. The broadcaster will send back a transaction id from which the receipt and event data will be retrieved as usual. + +Here's an example on how to file a motion through a metatransaction: + +```typescript +import { Id } from '@colony/sdk'; +// Immediately executing async function +(async function() { + // Create a motion in the Root team to create a new team using a metatransaction + const [{ motionId }] = await colony.createTeam().motionMeta(); +})(); +``` + +## tl;dir + +Okay, what did we learn? Here's a little overview: + +### Force a transaction + +- [[TxCreator.force]]: force a Colony transaction, knowing you have the permissions to do so +- [[TxCreator.forceMeta]]: same as `force()`, but send as a gasless metatransaction + +### Create a motion to trigger an action once it passes + +- [[TxCreator.motion]]: create a motion (needs the motion's domain as a parameter) +- [[TxCreator.forceMeta]]: same as `motion()`, but sends a gasless metatransaction + +Also refer to the [[TxCreator]] documentation if you'd like to learn more. diff --git a/examples/browser/src/advanced.ts b/examples/browser/src/advanced.ts index 16c02553..1ee8ccbd 100644 --- a/examples/browser/src/advanced.ts +++ b/examples/browser/src/advanced.ts @@ -1,8 +1,9 @@ import { providers, utils, Signer, ContractReceipt, BigNumber } from 'ethers'; -import { Colony, ColonyNetwork, toEth, w } from '../../../src'; +import { Colony, ColonyNetwork, PinataAdapter, toEth, w } from '../../../src'; const { isAddress } = utils; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const provider = new providers.Web3Provider((window as any).ethereum); let colony: Colony; @@ -10,7 +11,9 @@ const domainData: { fundingPotId?: BigNumber; domainId?: BigNumber } = {}; // Instantiate a colony client const getColony = async (colonyAddress: string, signer: Signer) => { - const colonyNetwork = new ColonyNetwork(signer); + const colonyNetwork = new ColonyNetwork(signer, { + ipfsAdapter: new PinataAdapter('INVALID'), + }); return colonyNetwork.getColony(colonyAddress); }; @@ -30,11 +33,11 @@ const createTeam = async (): Promise<{ domainPurpose?: string; }> => { // This is to demonstrate the Colony SDK's IPFS capabilities. For now, we would like to keep it agnostic to any IPFS upload mechanism, so you have to provide your own hash - // You can see how the data looks like here: https://cloudflare-ipfs.com/ipfs/QmVgJC8WNJCzkZYLPuVPG5gvSzLvLZTxvb24Sj5Nca4jW2 - const ipfsTestHash = 'QmTbb3TUiXiZifywgnkxBH5C1YLCyxnMww3Et3DCNypHB9'; - const [{ domainId, fundingPotId }, , getMetadata] = await colony.createTeam( - ipfsTestHash, - ); + // You can see how the data looks like here: https://cloudflare-ipfs.com/ipfs/QmTwksWE2Zn4icTvk5E7QZb1vucGNuu5GUCFZ361r8gKXM + const ipfsTestHash = 'QmTwksWE2Zn4icTvk5E7QZb1vucGNuu5GUCFZ361r8gKXM'; + const [{ domainId, fundingPotId }, , getMetadata] = await colony + .createTeam(ipfsTestHash) + .force(); if (!domainId || !fundingPotId || !getMetadata) { throw new Error('Transaction event data not found'); @@ -46,6 +49,14 @@ const createTeam = async (): Promise<{ throw new Error('No metadata found'); } + console.info(metadata); + + const [{ domainId: deprecatedDomain }] = await colony + .deprecateTeam(domainId, true) + .force(); + + console.info(`${deprecatedDomain} successfully deprecated`); + const { domainName, domainColor, domainPurpose } = metadata; return { @@ -63,17 +74,21 @@ const moveFunds = async (): Promise => { if (!domainData.domainId) { throw new Error('No domain created yet'); } - const [, receipt] = await colony.moveFundsToTeam( - w`0.66`, - domainData.domainId, - ); + const [, receipt] = await colony + .moveFundsToTeam(w`0.66`, domainData.domainId) + .force(); return receipt; }; // Make a payment to a user from the newly created and funded domain. This will cause the user to have reputation in the new domain after the next reputation mining cycle (max 24h) const makePayment = async (to: string): Promise => { + if (!colony.ext.oneTx) { + throw new Error('OneTxPayment extension not installed'); + } // Create payment in newly created domain - const [, receipt] = await colony.pay(to, w`0.42`, domainData.domainId); + const [, receipt] = await colony.ext.oneTx + .pay(to, w`0.42`, domainData.domainId) + .force(); return receipt; }; @@ -133,7 +148,8 @@ buttonConnect.addEventListener('click', async () => { const cc = await getColony(inputAddress.value, signer); colony = cc; const funding = await getColonyFunding(); - const tokenSymbol = await cc.colonyToken.symbol(); + const token = await cc.getToken(); + const tokenSymbol = await token.symbol(); speak(` Connected to Colony with address: ${colonyAddress}. Colony version: ${cc.version}. diff --git a/examples/browser/src/events.ts b/examples/browser/src/events.ts index 0cc65d5d..dae9d7ef 100644 --- a/examples/browser/src/events.ts +++ b/examples/browser/src/events.ts @@ -1,6 +1,6 @@ import { providers, utils } from 'ethers'; -import { ColonyEventManager } from '../../../src'; +import { ColonyEventManager, MetadataType } from '../../../src'; import type { ColonyEvent } from '../../../src'; const provider = new providers.JsonRpcProvider('https://xdai.colony.io/rpc2/'); @@ -9,7 +9,7 @@ const { isAddress } = utils; // This event listener will only list for the `DomainAdded` event in the Colony of the user's choice. Run this and then create a Team in that Colony, to see it being picked up here const setupEventListener = ( colonyAddress: string, - callback: (events: ColonyEvent[]) => void, + callback: (events: ColonyEvent[]) => void, ) => { const manager = new ColonyEventManager(provider); diff --git a/examples/browser/src/local-motions.ts b/examples/browser/src/local-motions.ts index 93afdd4a..dc589a0f 100644 --- a/examples/browser/src/local-motions.ts +++ b/examples/browser/src/local-motions.ts @@ -49,24 +49,34 @@ const installVotingReputation = async () => { await setupVotingReputationExtension(metaColony); // Re-initialize Colony after VotingReputationExtension is installed metaColony = await colonyNetwork.getMetaColony(); + if (!metaColony.ext.oneTx) { + throw new Error('OneTxPayment extension not installed'); + } // Mint CLNY and fund the Colony with it - await metaColony.colonyToken.mint(w`500`); + const token = await metaColony.getToken(); + await token.mint(w`500`).force(); // Claim the CLNY for the MetaColony (important!) - await metaColony.claimFunds(); + await metaColony.claimFunds().force(); // Pay some CLNY each to two addresses (we are going to use the first for staking) // This will also give these addresses reputation in the ROOT team - await metaColony.pay('0xb77D57F4959eAfA0339424b83FcFaf9c15407461', w`100`); - await metaColony.pay('0x9df24e73f40b2a911eb254a8825103723e13209c', w`20`); + await metaColony.ext.oneTx + .pay('0xb77D57F4959eAfA0339424b83FcFaf9c15407461', w`100`) + .force(); + await metaColony.ext.oneTx + .pay('0x9df24e73f40b2a911eb254a8825103723e13209c', w`20`) + .force(); }; const createPaymentMotion = async (amount: string): Promise => { if (!metaColony.ext.motions) { - throw new Error('Motions & Disputes extension not installed'); + throw new Error('VotingReputation extension not installed'); } - const [{ motionId }] = await metaColony.ext.motions.create.pay( - '0x27ff0c145e191c22c75cd123c679c3e1f58a4469', - toWei(amount), - ); + if (!metaColony.ext.oneTx) { + throw new Error('OneTxPayment extension not installed'); + } + const [{ motionId }] = await metaColony.ext.oneTx + .pay('0x27ff0c145e191c22c75cd123c679c3e1f58a4469', toWei(amount)) + .motion(); if (!motionId) { // This case should not happen (rather the tx reverts) but we're making the check here for type-safety @@ -97,7 +107,7 @@ const getMotion = async (motionId: BigNumberish) => { }; const approveForStaking = async () => { - const token = metaColony.colonyToken; + const token = await metaColony.getToken(); // This will activate 20 tokens for Motion staking, for the user address 0xb77D57F4959eAfA0339424b83FcFaf9c15407461. // Essentially you first "activate" them for use in the Colony in general and then approve some amount of that for staking in the VotingReputation extension await token.approve(w`20`); diff --git a/examples/browser/src/local-reputation.ts b/examples/browser/src/local-reputation.ts index 32fae04c..8e3d5e0c 100644 --- a/examples/browser/src/local-reputation.ts +++ b/examples/browser/src/local-reputation.ts @@ -35,10 +35,11 @@ const getMetaColony = async (networkAddress: string) => { // Mint CLNY and fund the Colony with it const fundColony = async (amount: string) => { + const token = await metaColony.getToken(); // Mint `amount` CLNY - await metaColony.colonyToken.mint(toWei(amount)); + await token.mint(toWei(amount)).force(); // Claim the CLNY for the MetaColony (important!) - await metaColony.claimFunds(); + await metaColony.claimFunds().force(); // Look up the funds const funding = await metaColony.getBalance(); return toEth(funding); @@ -46,8 +47,11 @@ const fundColony = async (amount: string) => { // Make a payment to the given user in the MetaColony's native token (CLNY). This will cause the user to have reputation in the new domain after the next reputation mining cycle (max 24h) const makePayment = async (to: string) => { + if (!metaColony.ext.oneTx) { + throw new Error('OneTxPayment extension not installed'); + } // Pay 10 CLNY to the recipient - return metaColony.pay(to, w`10`); + return metaColony.ext.oneTx.pay(to, w`10`).force(); }; // We're using Ganache's evm_increaseTime and evm_mine methods to first increase the block time artificially by one hour and then force a block to mine. This will trigger the local reputation oracle/miner to award the pending reputation. diff --git a/examples/browser/src/motions.ts b/examples/browser/src/motions.ts index b5500fa6..34f88b36 100644 --- a/examples/browser/src/motions.ts +++ b/examples/browser/src/motions.ts @@ -34,13 +34,12 @@ const connectColony = async (colonyAddress: string) => { }; const createPaymentMotion = async (amount: string): Promise => { - if (!colony.ext.motions) { + if (!colony.ext.motions || !colony.ext.oneTx) { throw new Error('Motions & Disputes extension not installed'); } - const [{ motionId }] = await colony.ext.motions.create.pay( - '0x27ff0c145e191c22c75cd123c679c3e1f58a4469', - toWei(amount), - ); + const [{ motionId }] = await colony.ext.oneTx + .pay('0x27ff0c145e191c22c75cd123c679c3e1f58a4469', toWei(amount)) + .motion(); if (!motionId) { // This case should not happen (rather the tx reverts) but we're making the check here for type-safety diff --git a/examples/node/metatx.ts b/examples/node/metatx.ts new file mode 100644 index 00000000..6e25b247 --- /dev/null +++ b/examples/node/metatx.ts @@ -0,0 +1,16 @@ +import { constants, providers, Wallet } from 'ethers'; + +import { ColonyNetwork } from '../../src'; + +const provider = new providers.JsonRpcProvider('https://xdai.colony.io/rpc2/'); + +// Claim ETH for the MetaColony using a Metatransaction +const start = async () => { + const signer = Wallet.createRandom().connect(provider); + const colonyNetwork = new ColonyNetwork(signer); + const colony = await colonyNetwork.getMetaColony(); + const res = await colony.claimFunds(constants.AddressZero).force(); + console.info(res); +}; + +start(); diff --git a/package-lock.json b/package-lock.json index 3a913804..c75d485a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { "name": "@colony/sdk", - "version": "0.7.0-beta.1", + "version": "0.7.0-beta.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@colony/sdk", - "version": "0.7.0-beta.1", + "version": "0.7.0-beta.2", "license": "GPL-3.0-only", "dependencies": { "@colony/colony-event-metadata-parser": "^1.1.5", - "@colony/colony-js": "^5.3.0-beta.1", + "@colony/colony-js": "^6.0.2", "@urql/core": "^2.5.0", "cross-fetch": "^3.1.5", "fetch-retry": "^5.0.2", @@ -364,9 +364,9 @@ } }, "node_modules/@colony/colony-js": { - "version": "5.3.0-beta.1", - "resolved": "https://registry.npmjs.org/@colony/colony-js/-/colony-js-5.3.0-beta.1.tgz", - "integrity": "sha512-3SiLUA44cqtfr8sPYclB5OYlnbU3Z4dSvcq4ZZJKWWGi1D4yfx7Ue3zYX2UQAUii9CbKn9ixOZhz56FWVCMuwQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@colony/colony-js/-/colony-js-6.0.2.tgz", + "integrity": "sha512-DAQV5PKi0tOQwApUk2gXNNV36glcn7ltyHCpi89SzaKOt7X04xEi5fl8cJug9iz2QUggIZowrmlIz5yOeB7EGw==", "dependencies": { "cross-fetch": "^3.1.5" }, @@ -375,7 +375,7 @@ "npm": "^8" }, "peerDependencies": { - "ethers": "^5.5.4" + "ethers": "^5.1.3" } }, "node_modules/@colony/eslint-config-colony": { @@ -8769,9 +8769,9 @@ } }, "@colony/colony-js": { - "version": "5.3.0-beta.1", - "resolved": "https://registry.npmjs.org/@colony/colony-js/-/colony-js-5.3.0-beta.1.tgz", - "integrity": "sha512-3SiLUA44cqtfr8sPYclB5OYlnbU3Z4dSvcq4ZZJKWWGi1D4yfx7Ue3zYX2UQAUii9CbKn9ixOZhz56FWVCMuwQ==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@colony/colony-js/-/colony-js-6.0.2.tgz", + "integrity": "sha512-DAQV5PKi0tOQwApUk2gXNNV36glcn7ltyHCpi89SzaKOt7X04xEi5fl8cJug9iz2QUggIZowrmlIz5yOeB7EGw==", "requires": { "cross-fetch": "^3.1.5" } diff --git a/package.json b/package.json index 174c3891..93870d08 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "@colony/sdk", - "version": "0.7.0-beta.1", + "version": "0.7.0-beta.2", "license": "GPL-3.0-only", "dependencies": { "@colony/colony-event-metadata-parser": "^1.1.5", - "@colony/colony-js": "^5.3.0-beta.1", + "@colony/colony-js": "^6.0.2", "@urql/core": "^2.5.0", "cross-fetch": "^3.1.5", "fetch-retry": "^5.0.2", diff --git a/src/ColonyNetwork/Colony.ts b/src/ColonyNetwork/Colony.ts index d5ff0958..a1d37fb5 100644 --- a/src/ColonyNetwork/Colony.ts +++ b/src/ColonyNetwork/Colony.ts @@ -2,7 +2,10 @@ import { ColonyClientV10, SignerOrProvider, Id, - Extension, + ColonyRole, + IBasicMetaTransaction, + getChildIndex, + getPermissionProofs, } from '@colony/colony-js'; import { AnnotationEventObject, @@ -11,29 +14,30 @@ import { // eslint-disable-next-line max-len ColonyFundsMovedBetweenFundingPots_address_uint256_uint256_uint256_address_EventObject, DomainAdded_uint256_EventObject, + DomainDeprecatedEventObject, DomainMetadataEventObject, FundingPotAddedEventObject, - OneTxPaymentMadeEventObject, } from '@colony/colony-js/extras'; -import { MetadataType } from '@colony/colony-event-metadata-parser'; - -import type { - BigNumberish, - BytesLike, - ContractReceipt, - ContractTransaction, -} from 'ethers'; +import { + AnnotationMetadata, + DomainMetadata, + MetadataType, +} from '@colony/colony-event-metadata-parser'; +import { BigNumberish, BytesLike, ContractReceipt } from 'ethers'; -import { MetadataEvent, MetadataEventValue } from '../ipfs'; import { extractEvent } from '../utils'; import { ColonyToken } from './ColonyToken'; import { ColonyNetwork } from './ColonyNetwork'; +import { OneTxPayment } from './OneTxPayment'; import { VotingReputation } from './VotingReputation'; +import { Expand, Parameters, ParametersFrom2 } from '../types'; +import { PermissionConfig, TxCreator } from './TxCreator'; export type SupportedColonyClient = ColonyClientV10; export type SupportedColonyMethods = SupportedColonyClient['functions']; export interface SupportedExtensions { motions?: VotingReputation; + oneTx?: OneTxPayment; } export class Colony { @@ -44,8 +48,6 @@ export class Colony { private colonyClient: SupportedColonyClient; - private signerOrProvider: SignerOrProvider; - address: string; colonyNetwork: ColonyNetwork; @@ -55,10 +57,12 @@ export class Colony { * * Currently only Tokens deployed via Colony are supported (no external, imported tokens) in Colony SDK. All other kinds will throw an error */ - colonyToken: ColonyToken; + colonyToken?: ColonyToken; ext: SupportedExtensions; + signerOrProvider: SignerOrProvider; + version: number; /** @@ -78,40 +82,93 @@ export class Colony { ) { this.colonyClient = colonyClient; this.colonyNetwork = colonyNetwork; - this.colonyToken = new ColonyToken(this); this.signerOrProvider = colonyClient.signer || colonyClient.provider; this.address = colonyClient.address; this.ext = {}; this.version = colonyClient.clientVersion; } - private async returnTxData< - D extends { metadata?: string }, - E extends MetadataEvent, + /** + * Creates a new [[TxCreator]] for non-permissioned Colony transactions + * @internal + * + * @remarks + * Do not use this method directly but rather the class methods in the Colony or extensions + * + * @param contract A ColonyJS contract + * @param method The transaction method to execute on the contract + * @param args The arguments for the method + * @param eventData A function that extracts the relevant event data from the [[ContractReceipt]] + * @param metadataType The [[MetadataType]] if the event contains metadata + * @returns A [[TxCreator]] + */ + createTxCreator< + C extends IBasicMetaTransaction, + F extends keyof C['functions'], + D extends Record, + M extends MetadataType, >( - data: D, - metadataEvent: E, - receipt: ContractReceipt, - ): Promise< - | [D, ContractReceipt, () => Promise>] - | [D, ContractReceipt] - > { - if (data.metadata) { - const getMetadata = this.colonyNetwork.ipfs.getMetadataForEvent.bind( - this.colonyNetwork.ipfs, - metadataEvent, - data.metadata, - ) as () => Promise>; - - return [data, receipt, getMetadata]; - } + contract: C, + method: F, + args: + | Parameters + | (() => Promise>), + eventData?: (receipt: ContractReceipt) => Promise, + metadataType?: M, + ) { + return new TxCreator({ + colony: this, + contract, + method, + args, + eventData, + metadataType, + }); + } - return [data, receipt]; + /** + * Creates a new [[TxCreator]] for permissioned Colony transactions + * @internal + * + * @remarks + * Do not use this method directly but rather the class methods in the Colony or extensions + * + * @param contract A ColonyJS contract + * @param method The transaction method to execute on the contract + * @param args The arguments for the method + * @param permissionConfig Relevant configuration for the permissioned Colony function + * @param eventData A function that extracts the relevant event data from the [[ContractReceipt]] + * @param metadataType The [[MetadataType]] if the event contains metadata + * @returns A permissioned [[TxCreator]] + */ + createPermissionedTxCreator< + C extends IBasicMetaTransaction, + F extends keyof C['functions'], + D extends Record, + M extends MetadataType, + >( + contract: C, + method: F, + args: + | ParametersFrom2 + | (() => Promise>), + permissionConfig: PermissionConfig, + eventData?: (receipt: ContractReceipt) => Promise, + metadataType?: M, + ) { + return new TxCreator({ + colony: this, + contract, + method, + args, + permissionConfig, + eventData, + metadataType, + }); } /** * Provide direct access to the internally used ColonyJS client. Only use when you know what you're doing - * * @internal * * @returns The internally used ColonyClient @@ -121,8 +178,25 @@ export class Colony { } /** - * Installs colony extensions that can be instantiated in the callback function + * Gets the colony's [[ColonyToken]]. Will instantiate it if it doesn't exist yet + * @internal * + * @returns The colony's [[ColonyToken]] + */ + async getToken(): Promise { + if (this.colonyToken) { + return this.colonyToken; + } + const tokenLockingClient = + // eslint-disable-next-line max-len + await this.getInternalColonyClient().networkClient.getTokenLockingClient(); + const token = new ColonyToken(this, tokenLockingClient); + this.colonyToken = token; + return this.colonyToken; + } + + /** + * Installs colony extensions that can be instantiated in the callback function * @internal */ installExtensions(extensions: SupportedExtensions) { @@ -161,14 +235,31 @@ export class Colony { } /** - * Create a team within a Colony + * Create a team (domain) within a Colony with team details as metadata * * @remarks * Currently you can only add domains within the `Root` domain. This restriction will be lifted soon * - * @param metadataCid An IPFS [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file containing the metadata described below. For now, we would like to keep it agnostic to any IPFS upload mechanism, so you have to upload the file manually and provide your own hash (by using, for example, [Pinata](https://docs.pinata.cloud/)) + * @example + * ```typescript + * import { TeamColor } from '@colony/sdk'; + * + * // Immediately executing async function + * (async function() { + * // Create team of the butter-passers + * // (forced transaction example) + * // (also notice that this requires an upload-capable IPFS adapter) + * await colony.createTeam({ + * domainName: 'Butter-passers', + * domainColor: TeamColor.Gold, + * domainPurpose: 'To pass butter', + * }).force(); + * })(); + * ``` + * + * @param metadata The team metadata you would like to add (or an IPFS CID pointing to valid metadata). If [[DomainMetadata]] is provided directly (as opposed to a [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file) this requires an [[IpfsAdapter]] that can upload and pin to IPFS (like the [[PinataAdapter]]). See its documentation for more information. * - * @returns A tupel: `[eventData, ContractReceipt, getMetaData]` + * @returns A [[TxCreator]] * * **Event data** * @@ -187,31 +278,131 @@ export class Colony { * | `domainColor` | string | The color assigned to this team | * | `domainPurpose` | string | The purpose for this team (a broad description) | */ - async createTeam(metadataCid?: string) { - let tx: ContractTransaction; - if (metadataCid) { - tx = await this.colonyClient['addDomainWithProofs(uint256,string)']( - Id.RootDomain, - metadataCid, - ); - } else { - tx = await this.colonyClient['addDomainWithProofs(uint256)']( - Id.RootDomain, + createTeam( + metadata: DomainMetadata | string, + ): TxCreator< + SupportedColonyClient, + 'addDomain(uint256,uint256,uint256,string)', + Expand< + DomainAdded_uint256_EventObject & + FundingPotAddedEventObject & { metadata: string } + >, + MetadataType.Domain + >; + + /** + * Create a team (domain) within a Colony with no metadata attached + * + * @remarks + * Currently you can only add domains within the `Root` domain. This restriction will be lifted soon + * + * @returns A [[TxCreator]] + * + * **Event data** + * + * | Property | Type | Description | + * | :------ | :------ | :------ | + * | `agent` | string | The address that is responsible for triggering this event | + * | `domainId` | BigNumber | Integer domain id of the created team | + * | `fundingPotId` | BigNumber | Integer id of the corresponding funding pot | + */ + createTeam(): TxCreator< + SupportedColonyClient, + 'addDomain(uint256,uint256,uint256,string)', + Expand< + DomainAdded_uint256_EventObject & + FundingPotAddedEventObject & { metadata: undefined } + >, + MetadataType + >; + + createTeam(metadata?: DomainMetadata | string) { + if (!metadata) { + return this.createPermissionedTxCreator( + this.colonyClient, + 'addDomain(uint256,uint256,uint256)', + [Id.RootDomain], + { + roles: ColonyRole.Architecture, + domain: Id.RootDomain, + }, + async (receipt) => ({ + ...extractEvent( + 'DomainAdded', + receipt, + ), + ...extractEvent( + 'FundingPotAdded', + receipt, + ), + }), ); } - const receipt = await tx.wait(); - - const data = { - ...extractEvent('DomainAdded', receipt), - ...extractEvent('FundingPotAdded', receipt), - ...extractEvent('DomainMetadata', receipt), - }; + return this.createPermissionedTxCreator( + this.colonyClient, + 'addDomain(uint256,uint256,uint256,string)', + async () => { + let cid: string; + if (typeof metadata == 'string') { + cid = metadata; + } else { + cid = await this.colonyNetwork.ipfs.uploadMetadata( + MetadataType.Domain, + metadata, + ); + } + return [Id.RootDomain, cid] as [BigNumberish, string]; + }, + { + roles: ColonyRole.Architecture, + domain: Id.RootDomain, + }, + async (receipt) => ({ + ...extractEvent( + 'DomainAdded', + receipt, + ), + ...extractEvent('FundingPotAdded', receipt), + ...extractEvent('DomainMetadata', receipt), + }), + MetadataType.Domain, + ); + } - return this.returnTxData( - data, - 'DomainMetadata(address,uint256,string)', - receipt, + /** + * Deprecate (remove) or undeprecate a team + * + * Teams can be deprecated which will remove them from the UI. As they can't be deleted you can always undeprecate a team by passing `false` as the second argument. + * + * @param teamId Team to be (un)deprecated + * @param deprecated `true`: Deprecate team; `false`: Undeprecate team + * + * @returns A [[TxCreator]] + * + * **Event data** + * + * | Property | Type | Description | + * | :------ | :------ | :------ | + * | `agent` | string | The address that is responsible for triggering this event | + * | `domainId` | BigNumber | The id of the team that was (un)deprecated | + * | `deprecated` | bool | Whether the team was deprecated or not | + */ + deprecateTeam(teamId: BigNumberish, deprecated: boolean) { + return this.createPermissionedTxCreator( + this.colonyClient, + 'deprecateDomain', + [teamId, deprecated], + { + roles: ColonyRole.Architecture, + domain: teamId, + }, + async (receipt) => ({ + ...extractEvent( + 'DomainDeprecated', + receipt, + ), + }), ); } @@ -239,9 +430,11 @@ export class Colony { * * Anyone can call this function. Claims funds _for_ the Colony that have been sent to the Colony's contract address or minted funds of the Colony's native token. This function _has_ to be called in order for the funds to appear in the Colony's treasury. You can provide a token address for the token to be claimed. Otherwise it will claim the outstanding funds of the Colony's native token * + * @remarks use `ethers.constants.AddressZero` to claim ETH. + * * @param tokenAddress The address of the token to claim the funds for. Default is the Colony's native token * - * @returns A tupel of event data and contract receipt + * @returns A [[TxCreator]] * * **Event data** * @@ -252,88 +445,20 @@ export class Colony { * | `fee` | BigNumber | The fee deducted for rewards | * | `payoutRemainder` | BigNumber | The remaining funds moved to the top-level domain pot | */ - async claimFunds( - tokenAddress: string = this.colonyClient.tokenClient.address, - ) { + claimFunds(tokenAddress?: string) { const token = tokenAddress || this.colonyClient.tokenClient.address; - const tx = await this.colonyClient.claimColonyFunds(token); - const receipt = await tx.wait(); - const data = { - ...extractEvent( - 'ColonyFundsClaimed', - receipt, - ), - }; - - return [data, receipt] as [typeof data, typeof receipt]; - } - - /** - * Make a payment to a single address using a single token - * - * @remarks Requires the `OneTxPayment` extension to be installed for the Colony (this is usually the case for Colonies created via the Dapp). Note that most tokens use 18 decimals, so add a bunch of zeros or use our `w` or `toWei` functions (see example) - * - * @example - * ```typescript - * import { Id, Tokens, w } from '@colony/sdk'; - * - * // Immediately executing async function - * (async function() { - * // Pay 10 XDAI (on Gnosis chain) from the root domain to the following address - * await colony.pay( - * '0xb77D57F4959eAfA0339424b83FcFaf9c15407461', - * w`10`, - * Id.RootDomain, - * Tokens.Gnosis.XDAI, - * ); - * })(); - * ``` - * - * @param recipient Wallet address of account to send the funds to (also awarded reputation when sending the native token) - * @param amount Amount to pay in wei - * @param tokenAddress The address of the token to make the payment in. Default is the Colony's native token - * @param teamId The team to use to send the funds from. Has to have funding of at least the amount you need to send. See [[Colony.moveFundsToTeam]]. Defaults to the Colony's root team - * @returns A tupel of event data and contract receipt - * - * **Event data** - * - * | Property | Type | Description | - * | :------ | :------ | :------ | - * | `agent` | string | The address that is responsible for triggering this event | - * | `fundamentalId` | BigNumber | The newly added payment id | - * | `nPayouts` | BigNumber | Number of payouts in total | - */ - async pay( - recipient: string, - amount: BigNumberish, - teamId?: BigNumberish, - tokenAddress?: string, - ) { - const setTeamId = teamId || Id.RootDomain; - const setTokenAddress = - tokenAddress || this.colonyClient.tokenClient.address; - - const oneTxClient = await this.colonyClient.getExtensionClient( - Extension.OneTxPayment, - ); - - const tx = await oneTxClient.makePaymentFundedFromDomainWithProofs( - // This function supports multiple receipients, amounts and tokens. Here only one of each is used - [recipient], - [setTokenAddress], - [amount], - setTeamId, - // Skill associated with this payment. Ignore for now - Id.SkillIgnore, + return this.createTxCreator( + this.colonyClient, + 'claimColonyFunds', + [token], + async (receipt) => ({ + ...extractEvent( + 'ColonyFundsClaimed', + receipt, + ), + }), ); - - const receipt = await tx.wait(); - const data = { - ...extractEvent('OneTxPaymentMade', receipt), - }; - - return [data, receipt] as [typeof data, typeof receipt]; } /** @@ -350,11 +475,12 @@ export class Colony { * // Immediately executing async function * (async function() { * // Move 10 of the native token from team 2 to team 3 + * // (forced transaction example) * await colony.moveFundsToTeam( * w`10`, * 2, * 3, - * ); + * ).force(); * })(); * ``` * @@ -362,7 +488,7 @@ export class Colony { * @param toTeam The team to transfer the funds to * @param fromTeam The team to transfer the funds from. Default is the Root team * @param tokenAddress The address of the token to be transferred. Default is the Colony's native token - * @returns A tupel of event data and contract receipt + * @returns A [[TxCreator]] * * **Event data** * @@ -374,7 +500,7 @@ export class Colony { * | `amount` | BigNumber | The amount that was transferred | * | `token` | string | The token address being transferred | */ - async moveFundsToTeam( + moveFundsToTeam( amount: BigNumberish, toTeam: BigNumberish, fromTeam?: BigNumberish, @@ -385,26 +511,67 @@ export class Colony { const setTokenAddress = tokenAddress || this.colonyClient.tokenClient.address; - const { fundingPotId: fromPot } = await this.colonyClient.getDomain( - setFromTeam, - ); - const { fundingPotId: toPot } = await this.colonyClient.getDomain(toTeam); - - const tx = await this.colonyClient[ + return this.createTxCreator( + this.colonyClient, // eslint-disable-next-line max-len - 'moveFundsBetweenPotsWithProofs(uint256,uint256,uint256,uint256,address)' - ](parentTeam, fromPot, toPot, amount, setTokenAddress); - const receipt = await tx.wait(); - - const data = { - // eslint-disable-next-line max-len - ...extractEvent( - 'ColonyFundsMovedBetweenFundingPots', - receipt, - ), - }; - - return [data, receipt] as [typeof data, typeof receipt]; + 'moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)', + async () => { + // Manual permission proofs are needed here + const [permissionDomainId, childSkillIndex] = await getPermissionProofs( + this.colonyClient, + parentTeam, + ColonyRole.Funding, + ); + + const fromChildSkillIndex = await getChildIndex( + this.colonyClient, + parentTeam, + setFromTeam, + ); + + const toChildSkillIndex = await getChildIndex( + this.colonyClient, + parentTeam, + toTeam, + ); + + const { fundingPotId: fromPot } = await this.colonyClient.getDomain( + setFromTeam, + ); + const { fundingPotId: toPot } = await this.colonyClient.getDomain( + toTeam, + ); + + return [ + permissionDomainId, + childSkillIndex, + parentTeam, + fromChildSkillIndex, + toChildSkillIndex, + fromPot, + toPot, + amount, + setTokenAddress, + ] as [ + BigNumberish, + BigNumberish, + BigNumberish, + BigNumberish, + BigNumberish, + BigNumberish, + BigNumberish, + BigNumberish, + string, + ]; + }, + async (receipt) => ({ + // eslint-disable-next-line max-len + ...extractEvent( + 'ColonyFundsMovedBetweenFundingPots', + receipt, + ), + }), + ); } /** @@ -446,7 +613,8 @@ export class Colony { * ```typescript * import { ERC721 } from '@colony/sdk'; * - * // Mint a NFT for address 0xb794f5ea0ba39494ce839613fffba74279579268 + * // Mint an NFT for address 0xb794f5ea0ba39494ce839613fffba74279579268 + * // (forced transaction example) * const encodedAction = ERC721.encodeFunctionData( * 'mintTo', * '0xb794f5ea0ba39494ce839613fffba74279579268', @@ -459,27 +627,22 @@ export class Colony { * '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d', * // encoded transaction from above * encodedAction - * ); + * ).force(); * })(); * ``` * * @param target Address of the contract to execute a method on * @param action Encoded action to execute - * @returns A tupel of event data and contract receipt + * @returns A [[TxCreator]] * * **No event data** */ - async makeArbitraryTransaction(target: string, action: BytesLike) { - const tx = await this.colonyClient.makeArbitraryTransactions( - [target], - [action], - false, + makeArbitraryTransaction(target: string, action: BytesLike) { + return this.createTxCreator( + this.colonyClient, + 'makeArbitraryTransactions', + [[target], [action], false], ); - const receipt = await tx.wait(); - - const data = {}; - - return [data, receipt] as [typeof data, typeof receipt]; } /** @@ -487,7 +650,7 @@ export class Colony { * * This will annotate a transaction with an arbitrary text message. This only really works for transactions that happened within this Colony. This will upload the text string to IPFS and connect the transaction to the IPFS hash accordingly. * - * @remarks Requires an [[IpfsAdapter]] that can upload and pin to IPFS. See its documentation for more information. Keep in mind that **the annotation itself is a transaction**. + * @remarks If [[AnnotationMetadata]] is provided directly (as opposed to a [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file) this requires an [[IpfsAdapter]] that can upload and pin to IPFS. See its documentation for more information. Keep in mind that **the annotation itself is a transaction**. * * @example * ```typescript @@ -495,21 +658,22 @@ export class Colony { * (async function() { * * // Create a motion to pay 10 of the native token to some (maybe your own?) address - * const [, { transactionHash }] = await colony.ext.motions.create.pay( + * // (forced transaction example) + * const [, { transactionHash }] = await colony.ext.oneTx.pay( * '0xb77D57F4959eAfA0339424b83FcFaf9c15407461', * w`10`, - * ); + * ).motion(); * // Annotate the motion transaction with a little explanation :) * await colony.annotateTransaction( * transactionHash, - * 'I am creating this motion because I think I deserve a little bonus' - * ); + * { annotationMsg: 'I am creating this motion because I think I deserve a little bonus' }, + * ).force(); * })(); * ``` * * @param txHash Transaction hash of the transaction to annotate (within the Colony) - * @param annotationMsg The text message you would like to annotate the transaction with - * @returns A tupel of event data and contract receipt + * @param annotationMetadata The annotation metadata you would like to annotate the transaction with (or an IPFS CID pointing to valid metadata) + * @returns A [[TxCreator]] * * **Event data** * @@ -519,22 +683,29 @@ export class Colony { * | `txHash` | BigNumber | The hash of the annotated transaction | * | `metadata` | BigNumber | The IPFS hash (CID) of the metadata object | */ - async annotateTransaction(txHash: string, annotationMsg: string) { - const cid = await this.colonyNetwork.ipfs.uploadMetadata( - MetadataType.Annotation, - { - annotationMsg, + annotateTransaction( + txHash: string, + annotationMetadata: AnnotationMetadata | string, + ) { + return this.createTxCreator( + this.colonyClient, + 'annotateTransaction', + async () => { + let cid: string; + if (typeof annotationMetadata == 'string') { + cid = annotationMetadata; + } else { + cid = await this.colonyNetwork.ipfs.uploadMetadata( + MetadataType.Annotation, + annotationMetadata, + ); + } + return [txHash, cid] as [string, string]; }, + async (receipt) => ({ + ...extractEvent('Annotation', receipt), + }), + MetadataType.Annotation, ); - - const tx = await this.colonyClient.annotateTransaction(txHash, cid); - - const receipt = await tx.wait(); - - const data = { - ...extractEvent('Annotation', receipt), - }; - - return [data, receipt] as [typeof data, typeof receipt]; } } diff --git a/src/ColonyNetwork/ColonyNetwork.ts b/src/ColonyNetwork/ColonyNetwork.ts index 406c630c..4fbe5544 100644 --- a/src/ColonyNetwork/ColonyNetwork.ts +++ b/src/ColonyNetwork/ColonyNetwork.ts @@ -12,6 +12,8 @@ import { getVotingReputationClient, VotingReputation, } from './VotingReputation'; +import { getOneTxPaymentClient, OneTxPayment } from './OneTxPayment'; +import { MetaTxBroadCasterEndpoint } from '../constants'; /** Additional options for the [[ColonyNetwork]] */ export interface ColonyNetworkOptions { @@ -19,6 +21,12 @@ export interface ColonyNetworkOptions { ipfsAdapter?: IpfsAdapter; /** Provide custom [[NetworkClientOptions]] for the ColonyJS client */ networkClientOptions?: NetworkClientOptions; + /** Provide a custom metatransaction broadcaster endpoint */ + metaTxBroadcasterEndpoint?: string; +} + +export interface ColonyNetworkConfig { + metaTxBroadcasterEndpoint?: string; } export class ColonyNetwork { @@ -26,8 +34,12 @@ export class ColonyNetwork { ipfs: IpfsMetadata; + network: Network; + networkClient: ColonyNetworkClient; + config: ColonyNetworkConfig; + /** * Creates a new instance to connect to the ColonyNetwork * @@ -54,15 +66,20 @@ export class ColonyNetwork { signerOrProvider: SignerOrProvider, options?: ColonyNetworkOptions, ) { - const network = options?.networkClientOptions?.networkAddress + this.network = options?.networkClientOptions?.networkAddress ? Network.Custom : Network.Xdai; this.ipfs = new IpfsMetadata(options?.ipfsAdapter); this.networkClient = getColonyNetworkClient( - network, + this.network, signerOrProvider, options?.networkClientOptions, ); + this.config = { + metaTxBroadcasterEndpoint: + options?.metaTxBroadcasterEndpoint || + MetaTxBroadCasterEndpoint[this.network], + }; this.signerOrProvider = signerOrProvider; } @@ -100,6 +117,13 @@ export class ColonyNetwork { // Ignore error here. Extension just won't be available. } + try { + const oneTxPaymentClient = await getOneTxPaymentClient(colonyClient); + extensions.oneTx = new OneTxPayment(colony, oneTxPaymentClient); + } catch (e) { + // Ignore error here. Extension just won't be available. + } + colony.installExtensions(extensions); return colony; diff --git a/src/ColonyNetwork/ColonyToken.ts b/src/ColonyNetwork/ColonyToken.ts index 267e94ee..cc04c030 100644 --- a/src/ColonyNetwork/ColonyToken.ts +++ b/src/ColonyNetwork/ColonyToken.ts @@ -19,7 +19,7 @@ export class ColonyToken { private tokenClient: ColonyTokenClient; - private tokenLockingClient?: TokenLockingClient; + tokenLockingClient: TokenLockingClient; // TODO: Add symbol, decimals // Get symbol in colonyJS. Use async getToken function @@ -34,24 +34,16 @@ export class ColonyToken { * @param colony A Colony instance * @returns A Colony Token abstaction instance */ - constructor(colony: Colony) { - this.colony = colony; + constructor(colony: Colony, tokenLockingClient: TokenLockingClient) { const colonyClient = colony.getInternalColonyClient(); if (colonyClient.tokenClient.tokenClientType !== TokenClientType.Colony) { throw new Error( `Requested token is not a token deployed with Colony. Please use the Erc20Token class`, ); } + this.colony = colony; this.tokenClient = colonyClient.tokenClient; - } - - async getTokenLockingClient() { - if (!this.tokenLockingClient) { - this.tokenLockingClient = await this.colony - .getInternalColonyClient() - .networkClient.getTokenLockingClient(); - } - return this.tokenLockingClient; + this.tokenLockingClient = tokenLockingClient; } /** @@ -73,22 +65,28 @@ export class ColonyToken { * ```typescript * import { w } from '@colony/sdk'; * - * const token = colony.getToken(); - * // Mint 100 tokens of the Colony's native token - * await token.mint(w`100`); - * // Claim the minted tokens for the Colony - * await colony.claimFunds(); + * // Immediately executing async function + * (async function() { + * const token = await colony.getToken(); + * // Mint 100 tokens of the Colony's native token + * // (forced transaction example) + * await token.mint(w`100`).force(); + * // Claim the minted tokens for the Colony + * // (forced transaction example) + * await colony.claimFunds().force(); + * })(); * ``` * * @param amount Amount of the token to be minted * - * @returns A tupel of event data and contract receipt + * @returns A [[TxCreator]] */ - async mint(amount: BigNumberish) { - const tx = await this.colony.getInternalColonyClient().mintTokens(amount); - const receipt = await tx.wait(); - const data = {}; - return [data, receipt] as [typeof data, typeof receipt]; + mint(amount: BigNumberish) { + return this.colony.createTxCreator( + this.colony.getInternalColonyClient(), + 'mintTokens', + [amount], + ); } /** @@ -100,11 +98,14 @@ export class ColonyToken { * ```typescript * import { w } from '@colony/sdk'; * - * const token = colony.getToken(); - * // Approve 100 tokens to be "activated" - * await token.approve(w`100`); - * // Deposit the tokens - * await token.deposit(w`100`); + * // Immediately executing async function + * (async function() { + * const token = await colony.getToken(); + * // Approve 100 tokens to be "activated" + * await token.approve(w`100`); + * // Deposit the tokens + * await token.deposit(w`100`); + * })(); * ``` * * @param amount Amount of the token to be approved @@ -120,9 +121,8 @@ export class ColonyToken { * | `wad` | BigNumber | Amount that was approved | */ async approve(amount: BigNumberish) { - const tokenLockingClient = await this.getTokenLockingClient(); const tx = await this.tokenClient.approve( - tokenLockingClient.address, + this.tokenLockingClient.address, amount, ); const receipt = await tx.wait(); @@ -141,11 +141,14 @@ export class ColonyToken { * ```typescript * import { w } from '@colony/sdk'; * - * const token = colony.getToken(); - * // Approve 100 tokens to be "activated" - * await token.approve(w`100`); - * // Deposit the tokens - * await token.deposit(w`100`); + * // Immediately executing async function + * (async function() { + * const token = await colony.getToken(); + * // Approve 100 tokens to be "activated" + * await token.approve(w`100`); + * // Deposit the tokens + * await token.deposit(w`100`); + * })(); * ``` * * @param amount Amount of the token to be deposited @@ -161,8 +164,7 @@ export class ColonyToken { * | `amount` | BigNumber | Amount that was deposited | */ async deposit(amount: BigNumberish) { - const tokenLockingClient = await this.getTokenLockingClient(); - const tx = await tokenLockingClient['deposit(address,uint256,bool)']( + const tx = await this.tokenLockingClient['deposit(address,uint256,bool)']( this.tokenClient.address, amount, false, @@ -186,9 +188,12 @@ export class ColonyToken { * ```typescript * import { w } from '@colony/sdk'; * - * const token = colony.getToken(); - * // Withdraw 100 tokens that were previously deposited - * await token.withdraw(w`100`); + * // Immediately executing async function + * (async function() { + * const token = await colony.getToken(); + * // Withdraw 100 tokens that were previously deposited + * await token.withdraw(w`100`); + * })(); * ``` * * @param amount Amount of the token to be withdrawn @@ -204,8 +209,7 @@ export class ColonyToken { * | `amount` | BigNumber | Amount that was withdrawn | */ async withdraw(amount: BigNumberish) { - const tokenLockingClient = await this.getTokenLockingClient(); - const tx = await tokenLockingClient['withdraw(address,uint256,bool)']( + const tx = await this.tokenLockingClient['withdraw(address,uint256,bool)']( this.tokenClient.address, amount, false, @@ -230,8 +234,7 @@ export class ColonyToken { * @returns The currently deposited balance of the Colony's native token */ async getUserDeposit(user: string) { - const tokenLockingClient = await this.getTokenLockingClient(); - const userLock = await tokenLockingClient.getUserLock( + const userLock = await this.tokenLockingClient.getUserLock( this.tokenClient.address, user, ); @@ -249,8 +252,7 @@ export class ColonyToken { * @returns The currently approved balance of the Colony's native token for the obligator */ async getUserApproval(user: string, obligator: string) { - const tokenLockingClient = await this.getTokenLockingClient(); - return tokenLockingClient.getApproval( + return this.tokenLockingClient.getApproval( user, this.tokenClient.address, obligator, diff --git a/src/ColonyNetwork/MotionCreator.ts b/src/ColonyNetwork/MotionCreator.ts deleted file mode 100644 index 51bf7d4d..00000000 --- a/src/ColonyNetwork/MotionCreator.ts +++ /dev/null @@ -1,354 +0,0 @@ -import type { BigNumberish, BytesLike } from 'ethers'; - -import { - Id, - Extension, - ColonyRole, - getPermissionProofs, - getMultiPermissionProofs, - getChildIndex, -} from '@colony/colony-js'; - -import { MotionCreatedEventObject } from '@colony/colony-js/extras'; - -import type { Colony, SupportedColonyMethods } from './Colony'; - -import { ParametersFrom2 } from '../types'; -import { extractEvent } from '../utils'; -import { SupportedVotingReputationClient } from './VotingReputation'; - -/** - * MotionCreator - * - * This is part of the [[VotingReputation]] class and not to be meant to instantiated directly. - * You can find an instance of this under `colony.ext.motions.create` - * - */ -export class MotionCreator { - private colony: Colony; - - private votingReputationClient: SupportedVotingReputationClient; - - constructor( - colony: Colony, - votingReputationClient: SupportedVotingReputationClient, - ) { - this.colony = colony; - this.votingReputationClient = votingReputationClient; - } - - private async createMotion( - signature: F, - actionDomain: BigNumberish, - actionRole: ColonyRole, - motionDomain: BigNumberish, - args: ParametersFrom2, - ) { - const colonyClient = this.colony.getInternalColonyClient(); - const [permissionDomainId, childSkillIndex] = await getPermissionProofs( - colonyClient, - actionDomain, - actionRole, - this.votingReputationClient.address, - ); - - const encodedAction = colonyClient.interface.encodeFunctionData( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore I don't think this can be typed properly here - signature, - [permissionDomainId, childSkillIndex, ...args], - ) as string; - - const tx = await this.votingReputationClient.createMotionWithProofs( - motionDomain, - '0x0', - encodedAction, - ); - - const receipt = await tx.wait(); - - const data = { - ...extractEvent('MotionCreated', receipt), - }; - - return [data, receipt] as [typeof data, typeof receipt]; - } - - /** - * Create a motion to create a team within a Colony - * - * For more information about the resulting action, see [[Colony.createTeam]]. - * - * @remarks - * Currently you can only add domains within the `Root` domain. This restriction will be lifted soon - * - * @param metadataCid An IPFS [CID](https://docs.ipfs.io/concepts/content-addressing/#identifier-formats) for a JSON file containing the metadata described below. For now, we would like to keep it agnostic to any IPFS upload mechanism, so you have to upload the file manually and provide your own hash (by using, for example, [Pinata](https://docs.pinata.cloud/)) - * - * @returns A tupel: `[eventData, ContractReceipt]` - * - * **Motion event data** - * - * | Property | Type | Description | - * | :------ | :------ | :------ | - * | `motionId` | BigNumber | ID of the motion created | - * | `creator` | string | Address of the motion's creator | - * | `domainId` | BigNumber | Team ID of the motion | - */ - async createTeam(metadataCid?: string) { - // This will change when we have nested permissions - const parentDomain = Id.RootDomain; - - return metadataCid - ? this.createMotion( - 'addDomain(uint256,uint256,uint256,string)', - parentDomain, - ColonyRole.Architecture, - parentDomain, - [parentDomain, metadataCid], - ) - : this.createMotion( - 'addDomain(uint256,uint256,uint256)', - parentDomain, - ColonyRole.Architecture, - parentDomain, - [parentDomain], - ); - } - - /** - * Create a motion to make a payment to a single address using a single token - * - * For more information about the resulting action, see [[Colony.pay]]. - * - * @remarks Requires the `OneTxPayment` extension to be installed for the Colony (this is usually the case for Colonies created via the Dapp). Note that most tokens use 18 decimals, so add a bunch of zeros or use our `w` or `toWei` functions (see example) - * - * @example - * ```typescript - * import { Id, Tokens, w } from '@colony/sdk'; - * - * // Immediately executing async function - * (async function() { - * // Pay 10 XDAI (on Gnosis chain) from the root domain to the following address - * await colony.ext.motions.create.pay( - * '0xb77D57F4959eAfA0339424b83FcFaf9c15407461', - * w`10`, - * Id.RootDomain, - * Tokens.Gnosis.XDAI, - * ); - * })(); - * ``` - * - * @param recipient Wallet address of account to send the funds to (also awarded reputation when sending the native token) - * @param amount Amount to pay in wei - * @param tokenAddress The address of the token to make the payment in. Default is the Colony's native token - * @param teamId The team to use to send the funds from. Has to have funding of at least the amount you need to send. See [[Colony.moveFundsToTeam]]. Defaults to the Colony's root team - * @returns A tupel of event data and contract receipt - * - * **Motion event data** - * - * | Property | Type | Description | - * | :------ | :------ | :------ | - * | `motionId` | BigNumber | ID of the motion created | - * | `creator` | string | Address of the motion's creator | - * | `domainId` | BigNumber | Team ID of the motion | - */ - async pay( - recipient: string, - amount: BigNumberish, - teamId?: BigNumberish, - tokenAddress?: string, - ) { - const colonyClient = this.colony.getInternalColonyClient(); - const setTeamId = teamId || Id.RootDomain; - const setTokenAddress = tokenAddress || colonyClient.tokenClient.address; - - const oneTxClient = await colonyClient.getExtensionClient( - Extension.OneTxPayment, - ); - - const [extensionPDID, extensionCSI] = await getMultiPermissionProofs( - colonyClient, - setTeamId, - [ColonyRole.Funding, ColonyRole.Administration], - oneTxClient.address, - ); - const [userPDID, userCSI] = await getMultiPermissionProofs( - colonyClient, - setTeamId, - [ColonyRole.Funding, ColonyRole.Administration], - ); - - const encodedAction = oneTxClient.interface.encodeFunctionData( - 'makePaymentFundedFromDomain', - [ - extensionPDID, - extensionCSI, - userPDID, - userCSI, - [recipient], - [setTokenAddress], - [amount], - setTeamId, - // Skill associated with this payment. Ignore for now - Id.SkillIgnore, - ], - ); - - const tx = await this.votingReputationClient.createMotionWithProofs( - setTeamId, - oneTxClient.address, - encodedAction, - ); - - const receipt = await tx.wait(); - - const data = { - ...extractEvent('MotionCreated', receipt), - }; - - return [data, receipt] as [typeof data, typeof receipt]; - } - - /** - * Create a motion to move funds from one team to another - * - * For more information about the resulting action, see [[Colony.moveFundsToTeam]]. - * - * After sending funds to and claiming funds for your Colony they will land in a special team, the root team. If you want to make payments from other teams (in order to award reputation in that team) you have to move the funds there first. Use this method to do so. - * - * @example - * ```typescript - * import { Tokens } from '@colony/sdk'; - * - * // Immediately executing async function - * (async function() { - * // Move 10 of the native token from team 2 to team 3 - * await colony.ext.motions.create.moveFundsToTeam( - * w`10`, - * 2, - * 3, - * ); - * })(); - * ``` - * - * @param amount Amount to transfer between the teams - * @param toTeam The team to transfer the funds to - * @param fromTeam The team to transfer the funds from. Default is the Root team - * @param tokenAddress The address of the token to be transferred. Default is the Colony's native token - * @returns A tupel of event data and contract receipt - * - * **Motion event data** - * - * | Property | Type | Description | - * | :------ | :------ | :------ | - * | `motionId` | BigNumber | ID of the motion created | - * | `creator` | string | Address of the motion's creator | - * | `domainId` | BigNumber | Team ID of the motion | - */ - async moveFundsToTeam( - amount: BigNumberish, - toTeam: BigNumberish, - fromTeam?: BigNumberish, - tokenAddress?: string, - ) { - const parentTeam = Id.RootDomain; - const setFromTeam = fromTeam || Id.RootDomain; - const colonyClient = this.colony.getInternalColonyClient(); - const setTokenAddress = tokenAddress || colonyClient.tokenClient.address; - - const { fundingPotId: fromPot } = await colonyClient.getDomain(setFromTeam); - const { fundingPotId: toPot } = await colonyClient.getDomain(toTeam); - - const fromChildSkillIndex = await getChildIndex( - colonyClient, - parentTeam, - setFromTeam, - ); - const toChildSkillIndex = await getChildIndex( - colonyClient, - parentTeam, - toTeam, - ); - - return this.createMotion( - // eslint-disable-next-line max-len - 'moveFundsBetweenPots(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,address)', - parentTeam, - ColonyRole.Funding, - parentTeam, - [ - parentTeam, - fromChildSkillIndex, - toChildSkillIndex, - fromPot, - toPot, - amount, - setTokenAddress, - ], - ); - } - - /** - * Create a motion to create an arbitrary transaction in the name of the Colony - * - * For more information about the resulting action, see [[Colony.makeArbitraryTransaction]]. - * - * This method can execute a transaction on any Ethereum Smart Contract with the Colony address as the sender. The action needs to be encoded function data (see https://docs.ethers.io/v5/api/utils/abi/interface/#Interface--encoding). We provide some common interfaces for you to make it even easier. - * - * @example Create a motion to mint an NFT from a Colony - * ```typescript - * import { ERC721 } from '@colony/sdk'; - * - * // Mint a NFT for address 0xb794f5ea0ba39494ce839613fffba74279579268 - * const encodedAction = ERC721.encodeFunctionData( - * 'mintTo', - * '0xb794f5ea0ba39494ce839613fffba74279579268', - * ); - * - * // Immediately executing async function - * (async function() { - * await colony.ext.motions.create.makeArbitraryTransaction( - * // NFT contract address - * '0x06012c8cf97BEaD5deAe237070F9587f8E7A266d', - * // encoded transaction from above - * encodedAction - * ); - * })(); - * ``` - * - * @param target Address of the contract to execute a method on - * @param action Encoded action to execute - * @returns A tupel of event data and contract receipt - * - * **Motion event data** - * - * | Property | Type | Description | - * | :------ | :------ | :------ | - * | `motionId` | BigNumber | ID of the motion created | - * | `creator` | string | Address of the motion's creator | - * | `domainId` | BigNumber | Team ID of the motion | - */ - async makeArbitraryTransaction(target: string, action: BytesLike) { - const encodedAction = this.colony - .getInternalColonyClient() - .interface.encodeFunctionData('makeArbitraryTransactions', [ - [target], - [action], - false, - ]) as string; - - const tx = await this.votingReputationClient.createMotionWithProofs( - Id.RootDomain, - '0x0', - encodedAction, - ); - - const receipt = await tx.wait(); - - const data = { - ...extractEvent('MotionCreated', receipt), - }; - - return [data, receipt] as [typeof data, typeof receipt]; - } -} diff --git a/src/ColonyNetwork/OneTxPayment.ts b/src/ColonyNetwork/OneTxPayment.ts new file mode 100644 index 00000000..879d744b --- /dev/null +++ b/src/ColonyNetwork/OneTxPayment.ts @@ -0,0 +1,157 @@ +import { + ColonyRole, + Extension, + getPermissionProofs, + Id, + OneTxPaymentClientV3, +} from '@colony/colony-js'; + +import { OneTxPaymentMadeEventObject } from '@colony/colony-js/extras'; +import { BigNumber, BigNumberish } from 'ethers'; +import { extractEvent } from '../utils'; + +import { Colony, SupportedColonyClient } from './Colony'; + +export type SupportedOneTxPaymentClient = OneTxPaymentClientV3; +export const SUPPORTED_ONE_TX_PAYMENT_VERSION = 3; + +export const getOneTxPaymentClient = async ( + colonyClient: SupportedColonyClient, +) => { + const oneTxPaymentClient = await colonyClient.getExtensionClient( + Extension.OneTxPayment, + ); + + if (oneTxPaymentClient.clientVersion !== SUPPORTED_ONE_TX_PAYMENT_VERSION) { + throw new Error( + `The installed version ${oneTxPaymentClient.clientVersion} of the OneTxPayment extension is not supported. Please upgrade the extension in your Colony`, + ); + } + + return oneTxPaymentClient; +}; + +/** + * ## `OneTxPayment` (One Transaction Payment) + * + * Ordinarily payments require more than one transaction, because the payment lifecycle requires more than one permissioned role. + * + * In some use cases, there might be a need for one authorized individual to be able to create, funds, and finalize a payment within a single transaction. + * + * The OneTxPayment extension adds this functionality by adding a makePayment function which requires the caller to have both Funding and administration ability within the domain of the payment. + * + * Extension therefore requires Administration and Funding roles to function. + * + * Note: if you deployed your Colony using the Dapp, the OneTxPayment extension is already installed for you + */ +export class OneTxPayment { + private colony: Colony; + + private oneTxPaymentClient: SupportedOneTxPaymentClient; + + address: string; + + constructor(colony: Colony, oneTxPaymentClient: SupportedOneTxPaymentClient) { + this.address = oneTxPaymentClient.address; + this.colony = colony; + this.oneTxPaymentClient = oneTxPaymentClient; + } + + /** + * Make a payment to a single address using a single token + * + * @remarks Requires the `OneTxPayment` extension to be installed for the Colony (this is usually the case for Colonies created via the Dapp). Note that most tokens use 18 decimals, so add a bunch of zeros or use our `w` or `toWei` functions (see example) + * + * @example + * ```typescript + * import { Id, Tokens, w } from '@colony/sdk'; + * + * // Immediately executing async function + * (async function() { + * // Pay 10 XDAI (on Gnosis chain) from the root domain to the following address + * // (forced transaction example) + * await colony.pay( + * '0xb77D57F4959eAfA0339424b83FcFaf9c15407461', + * w`10`, + * Id.RootDomain, + * Tokens.Gnosis.XDAI, + * ).force(); + * })(); + * ``` + * + * @param recipient Wallet address of account to send the funds to (also awarded reputation when sending the native token) + * @param amount Amount to pay in wei + * @param tokenAddress The address of the token to make the payment in. Default is the Colony's native token + * @param teamId The team to use to send the funds from. Has to have funding of at least the amount you need to send. See [[Colony.moveFundsToTeam]]. Defaults to the Colony's root team + * @returns A [[TxCreator]] + * + * **Event data** + * + * | Property | Type | Description | + * | :------ | :------ | :------ | + * | `agent` | string | The address that is responsible for triggering this event | + * | `fundamentalId` | BigNumber | The newly added payment id | + * | `nPayouts` | BigNumber | Number of payouts in total | + */ + pay( + recipient: string, + amount: BigNumberish, + teamId?: BigNumberish, + tokenAddress?: string, + ) { + const setTeamId = teamId || Id.RootDomain; + const colonyClient = this.colony.getInternalColonyClient(); + + const setTokenAddress = tokenAddress || colonyClient.tokenClient.address; + + return this.colony.createTxCreator( + this.oneTxPaymentClient, + 'makePaymentFundedFromDomain', + async () => { + // Manual permission proofs are needed here + const [extPermissionDomainId, extChildSkillIndex] = + await getPermissionProofs( + colonyClient, + setTeamId, + [ColonyRole.Administration, ColonyRole.Funding], + this.oneTxPaymentClient.address, + ); + + const [userPermissionDomainId, userChildSkillIndex] = + await getPermissionProofs(colonyClient, setTeamId, [ + ColonyRole.Administration, + ColonyRole.Funding, + ]); + + return [ + extPermissionDomainId, + extChildSkillIndex, + userPermissionDomainId, + userChildSkillIndex, + [recipient], + [setTokenAddress], + [amount], + setTeamId, + // Skill associated with this payment. Ignore for now + Id.SkillIgnore, + ] as [ + BigNumber, + BigNumberish, + BigNumberish, + BigNumberish, + string[], + string[], + BigNumberish[], + BigNumberish, + BigNumberish, + ]; + }, + async (receipt) => ({ + ...extractEvent( + 'OneTxPaymentMade', + receipt, + ), + }), + ); + } +} diff --git a/src/ColonyNetwork/TxCreator.ts b/src/ColonyNetwork/TxCreator.ts new file mode 100644 index 00000000..32d1e421 --- /dev/null +++ b/src/ColonyNetwork/TxCreator.ts @@ -0,0 +1,393 @@ +import { + ColonyRole, + IBasicMetaTransaction, + getPermissionProofs, + getCreateMotionProofs, + Id, + Network, +} from '@colony/colony-js'; +import { + BigNumberish, + ContractReceipt, + ContractTransaction, + Signer, + utils, +} from 'ethers'; +import { fetch } from 'cross-fetch'; + +import { MotionCreatedEventObject } from '@colony/colony-js/extras'; +import { Colony } from './Colony'; +import { MetadataType, MetadataValue } from '../ipfs'; +import { extractEvent } from '../utils'; +import { ParsedLogTransactionReceipt } from '../types'; +import { IPFS_METADATA_EVENTS } from '../ipfs/IpfsMetadata'; + +const { arrayify, solidityKeccak256, splitSignature } = utils; + +export interface PermissionConfig { + domain: BigNumberish; + roles: ColonyRole | ColonyRole[]; + address?: string; +} + +interface EventData { + metadata?: string; +} + +type MetaTxFunctions = IBasicMetaTransaction['functions']; + +interface Functions extends MetaTxFunctions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: (...args: any[]) => Promise; +} + +interface BaseContract { + address: string; + functions: Functions; + interface: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + encodeFunctionData(functionFragment: string, values: any[]): string; + parseLog: IBasicMetaTransaction['interface']['parseLog']; + }; +} + +/** + * An umbrella API for all kinds of transactions + * + * The `TxCreator` allows for a simple API to cover all the different cases of transactions within Colony. Once a `TxCreator` is created using a method on the base contracts (e.g. [[Colony]] or extensions like [[VotingReputation]]), there are four options available: + * + * ## Force a transaction + * + * - [[TxCreator.force]]: force a Colony transaction, knowing you have the permissions to do so + * - [[TxCreator.forceMeta]]: same as `force()`, but send as a gasless metatransaction + * + * ## Create a motion to trigger an action once it passes + * + * - [[TxCreator.motion]]: create a motion (needs the motion's domain as a parameter) + * - [[TxCreator.forceMeta]]: same as `motion()`, but sends a gasless metatransaction + * + * Learn more about these functions in their individual documentation + */ +export class TxCreator< + C extends BaseContract, + M extends keyof C['functions'], + E extends EventData, + MD extends MetadataType, +> { + private colony: Colony; + + private contract: C; + + private method: string; + + private args: unknown[] | (() => Promise); + + private eventData?: (receipt: ContractReceipt) => Promise; + + private metadataType?: MD; + + private permissionConfig?: PermissionConfig; + + constructor({ + colony, + contract, + method, + args, + eventData, + metadataType, + permissionConfig, + }: { + colony: Colony; + contract: C; + method: M; + args: unknown[] | (() => Promise); + eventData?: (receipt: ContractReceipt) => Promise; + metadataType?: MD; + permissionConfig?: PermissionConfig; + }) { + this.colony = colony; + this.contract = contract; + this.method = method as string; + this.args = args; + this.eventData = eventData; + this.metadataType = metadataType; + this.permissionConfig = permissionConfig; + } + + private async getArgs() { + let args: unknown[] = []; + + if (typeof this.args == 'function') { + args = await this.args(); + } else { + args = this.args; + } + + if (this.permissionConfig) { + const [permissionDomainId, childSkillIndex] = await getPermissionProofs( + this.colony.getInternalColonyClient(), + this.permissionConfig.domain, + this.permissionConfig.roles, + this.permissionConfig.address, + ); + // Quite dangerous but probably fine. Plus, it gets TS off our backs ;) + args.unshift(permissionDomainId, childSkillIndex); + } + + return args; + } + + private async getEventData< + R extends ContractReceipt | ParsedLogTransactionReceipt, + >(receipt: R): Promise<[E, R, () => Promise>] | [E, R]> { + if (this.eventData) { + const data = await this.eventData(receipt); + + if (this.metadataType && data.metadata) { + const getMetadata = + this.colony.colonyNetwork.ipfs.getMetadataForEvent.bind( + this.colony.colonyNetwork.ipfs, + IPFS_METADATA_EVENTS[this.metadataType], + data.metadata, + ) as () => Promise>; + + return [data, receipt as R, getMetadata]; + } + + return [data, receipt as R]; + } + + return [{} as E, receipt as R]; + } + + private getSigner(): Signer { + const { signerOrProvider } = this.colony; + if (!(signerOrProvider instanceof Signer)) { + throw new Error('Need a signer to create a transaction'); + } + return signerOrProvider; + } + + private async sendMetaTransaction( + encodedTransaction: string, + target: string, + ): Promise { + const { colonyNetwork } = this.colony; + + if (!colonyNetwork.config.metaTxBroadcasterEndpoint) { + throw new Error( + `No metatransaction broadcaster endpoint found for network ${colonyNetwork.network}`, + ); + } + + const signer = this.getSigner(); + const { provider } = signer; + + if (!provider) { + throw new Error('No provider found'); + } + + let chainId: number; + + if (colonyNetwork.network === Network.Custom) { + chainId = 1; + } else { + const networkInfo = await provider.getNetwork(); + chainId = networkInfo.chainId; + } + + const userAddress = await signer.getAddress(); + const nonce = await this.contract.functions.getMetatransactionNonce( + userAddress, + ); + + const message = solidityKeccak256( + ['uint256', 'address', 'uint256', 'bytes'], + [nonce.toString(), target, chainId, encodedTransaction], + ); + const buf = arrayify(message); + const signature = await signer.signMessage(buf); + const { r, s, v } = splitSignature(signature); + + const broadcastData = { + target, + payload: encodedTransaction, + userAddress, + r, + s, + v, + }; + + const res = await fetch( + `${colonyNetwork.config.metaTxBroadcasterEndpoint}/broadcast`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(broadcastData), + }, + ); + const parsed = await res.json(); + + if (parsed.status !== 'success') { + throw new Error( + `Could not send Metatransaction. Reason given: ${parsed.data.reason}`, + ); + } + + if (!parsed.data?.txHash) { + throw new Error( + 'Could not get transaction hash from broadcaster response', + ); + } + + const receipt = (await provider.waitForTransaction( + parsed.data.txHash, + )) as ParsedLogTransactionReceipt; + receipt.parsedLogs = receipt.logs.map((log) => + this.contract.interface.parseLog(log), + ); + + return receipt; + } + + /** + * Forces an action + * + * @remarks The user sending this transaction has to have the appropriate permissions to do so. Learn more about permissions in Colony [here](/develop/dev-learning/permissions). + * + * @returns A tupel of event data and contract receipt (and a function to retrieve metadata if applicable) + */ + async force() { + const args = await this.getArgs(); + + const tx = (await this.contract.functions[this.method].apply( + this.contract, + args, + )) as ContractTransaction; + const receipt = await tx.wait(); + return this.getEventData(receipt); + } + + /** + * Creates a motion for an action + * + * You can specify a team (domain) this motion should be created in. It will be created in the Root team by default. + * + * @remarks This will only work if the [[VotingReputation]] extension is installed for the Colony that's being acted on + * + * @returns A tupel of motion event data and contract receipt + */ + async motion(motionDomain: BigNumberish = Id.RootDomain) { + if (!this.colony.ext.motions) { + throw new Error( + 'VotingReputation extension is not installed for this Colony', + ); + } + + const args = await this.getArgs(); + + const encodedAction = this.contract.interface.encodeFunctionData( + this.method, + args, + ); + + // If the contract for this TxCreator is the colony, use 0x0, otherwise use the extension's address + const altTarget = + this.contract.address === this.colony.address + ? '0x0' + : this.contract.address; + + return this.colony.ext.motions.createMotion( + motionDomain, + encodedAction, + altTarget, + ); + } + + /** + * Forces an action using a gasless metatransaction + * + * @remarks The user sending this transaction has to have the appropriate permissions to do so. Learn more about permissions in Colony [here](/develop/dev-learning/permissions). + * + * @returns A tupel of event data and contract receipt (and a function to retrieve metadata if applicable) + */ + async forceMeta() { + const args = await this.getArgs(); + + const encodedTransaction = this.contract.interface.encodeFunctionData( + this.method, + args, + ); + const receipt = await this.sendMetaTransaction( + encodedTransaction, + this.contract.address, + ); + return this.getEventData(receipt); + } + + /** + * Creates a motion for an action + * + * You can specify a team (domain) this motion should be created in. It will be created in the Root team by default. + * + * @remarks This will only work if the [[VotingReputation]] extension is installed for the Colony that's being acted on + * + * @returns A tupel of motion event data and contract receipt + */ + async motionMeta(motionDomain: BigNumberish = Id.RootDomain) { + if (!this.colony.ext.motions) { + throw new Error( + 'VotingReputation extension is not installed for this Colony', + ); + } + + const args = await this.getArgs(); + + const encodedAction = this.contract.interface.encodeFunctionData( + this.method, + args, + ); + + // If the contract for this TxCreator is the colony, use 0x0, otherwise use the extension's address + const altTarget = + this.contract.address === this.colony.address + ? '0x0' + : this.contract.address; + + const votingReputationClient = + this.colony.ext.motions.getInternalVotingReputationClient(); + + const { actionCid, key, value, branchMask, siblings } = + await getCreateMotionProofs( + votingReputationClient, + motionDomain, + altTarget, + encodedAction, + ); + + const encodedTransaction = + votingReputationClient.interface.encodeFunctionData('createMotion', [ + motionDomain, + actionCid, + altTarget, + encodedAction, + key, + value, + branchMask, + siblings, + ]); + + const receipt = await this.sendMetaTransaction( + encodedTransaction, + votingReputationClient.address, + ); + + const data = { + ...extractEvent('MotionCreated', receipt), + }; + + return [data, receipt] as [typeof data, typeof receipt]; + } +} diff --git a/src/ColonyNetwork/VotingReputation.ts b/src/ColonyNetwork/VotingReputation.ts index 7e6eeee1..7fb067fd 100644 --- a/src/ColonyNetwork/VotingReputation.ts +++ b/src/ColonyNetwork/VotingReputation.ts @@ -13,12 +13,12 @@ import { MotionVoteRevealedEventObject, MotionVoteSubmittedEventObject, UserTokenApprovedEventObject, + MotionCreatedEventObject, } from '@colony/colony-js/extras'; import { BigNumber, BigNumberish, Signer, utils } from 'ethers'; -import { extractEvent, extractEventFromLogs, toEth } from '../utils'; +import { extractEvent, extractCustomEvent, toEth } from '../utils'; import { Colony, SupportedColonyClient } from './Colony'; -import { MotionCreator } from './MotionCreator'; export type SupportedVotingReputationClient = VotingReputationClientV7; export const SUPPORTED_VOTING_REPUTATION_VERSION = 7; @@ -74,9 +74,9 @@ const REP_DIVISOR = BigNumber.from(10).pow(18); * * #### Creating a Motion * - * See [[MotionCreator]]. + * See [[TxCreator.motion]] or [[TxCreator.motionMeta]]. * - * Anyone within a Colony can start a motion. In Colony SDK, this can be done with the [[MotionCreator]] API. There the `action` (the contract transaction) for the Motion will be defined. This is essentially nothing else than an encoded contract function string alongside its parameters (see for detailed info [here](https://medium.com/linum-labs/a-technical-primer-on-using-encoded-function-calls-50e2b9939223) - but don't worry. In Colony SDK this will all be taken care of by the [[MotionCreator]]). + * Anyone within a Colony can start a motion. In Colony SDK, this is as easy as sending a transaction of the same kind. There the `action` (the contract transaction) for the Motion will be defined. This is essentially nothing else than an encoded contract function string alongside its parameters (see for detailed info [here](https://medium.com/linum-labs/a-technical-primer-on-using-encoded-function-calls-50e2b9939223) - but don't worry. In Colony SDK this will all be taken care of by the [[TxCreator]]). * * #### Staking * @@ -142,22 +142,16 @@ const REP_DIVISOR = BigNumber.from(10).pow(18); export class VotingReputation { private colony: Colony; - private signer: Signer; - private votingReputationClient: SupportedVotingReputationClient; address: string; - create: MotionCreator; - constructor( colony: Colony, votingReputationClient: SupportedVotingReputationClient, ) { this.address = votingReputationClient.address; - this.create = new MotionCreator(colony, votingReputationClient); this.colony = colony; - this.signer = this.colony.getInternalColonyClient().signer; this.votingReputationClient = votingReputationClient; } @@ -221,6 +215,47 @@ export class VotingReputation { return sideVoted; } + /** + * Provide direct access to the internally used ColonyJS client. Only use when you know what you're doing + * @internal + * + * @returns The internally used VotingReputationClient + */ + getInternalVotingReputationClient(): SupportedVotingReputationClient { + return this.votingReputationClient; + } + + /** + * Create a motion using an encoded action + * + * @remarks You will usually not use this function directly, but use the `send` or `motion` functions of the [[TxCreator]] within the relevant contract. + * + * @param motionDomain The domain the motion will be created in + * @param encodedAction The encoded action as a string + * @param altTarget The contract to which we send the action - 0x0 for the colony (default) + * + * @returns A Motion object + */ + async createMotion( + motionDomain: BigNumberish, + encodedAction: string, + altTarget = '0x0', + ) { + const tx = await this.votingReputationClient.createMotionWithProofs( + motionDomain, + altTarget, + encodedAction, + ); + + const receipt = await tx.wait(); + + const data = { + ...extractEvent('MotionCreated', receipt), + }; + + return [data, receipt] as [typeof data, typeof receipt]; + } + /** * Get a motion by its id * @@ -280,7 +315,7 @@ export class VotingReputation { /** * Get the minimum stake that has to be supplied for a motion and a certain vote (NOT for activation) * - * @remarks To get the missing amount for activation, call [[getMotionStakes]] + * @remarks To get the missing amount for activation, call [[getRemainingStakes]] * * @param motion A Motion struct object * @param vote A vote for (Yay) or against (Nay) the motion @@ -404,14 +439,13 @@ export class VotingReputation { .approveStake(this.votingReputationClient.address, teamId, amount); const receipt = await tx.wait(); - const tokenLockingClient = - await this.colony.colonyToken.getTokenLockingClient(); + const token = await this.colony.getToken(); const data = { - ...extractEventFromLogs( + ...extractCustomEvent( 'UserTokenApproved', receipt, - tokenLockingClient.interface, + token.tokenLockingClient.interface, ), }; @@ -456,7 +490,11 @@ export class VotingReputation { * | `eventIndex` | BigNumber | If the stake triggered a motion event, this will contain its index | */ async stakeMotion(motionId: BigNumberish, vote: Vote, amount: BigNumberish) { - const userAddress = await this.signer.getAddress(); + if (!(this.colony.signerOrProvider instanceof Signer)) { + throw new Error('Need a signer to create a transaction'); + } + + const userAddress = await this.colony.signerOrProvider.getAddress(); const motionState = await this.votingReputationClient.getMotionState( motionId, @@ -469,13 +507,14 @@ export class VotingReputation { } const motion = await this.getMotion(motionId); + const token = await this.colony.getToken(); - const deposited = await this.colony.colonyToken.getUserDeposit(userAddress); + const deposited = await token.getUserDeposit(userAddress); if (deposited.lt(amount)) { throw new Error('Not enough tokens deposited for staking.'); } - const colonyApproval = await this.colony.colonyToken.getUserApproval( + const colonyApproval = await token.getUserApproval( userAddress, this.colony.address, ); diff --git a/src/ColonyNetwork/index.ts b/src/ColonyNetwork/index.ts index 7272ca2f..6e0cef75 100644 --- a/src/ColonyNetwork/index.ts +++ b/src/ColonyNetwork/index.ts @@ -4,7 +4,7 @@ */ export { ColonyNetwork, ColonyNetworkOptions } from './ColonyNetwork'; -export { Colony } from './Colony'; +export { Colony, SupportedExtensions } from './Colony'; export { ColonyToken } from './ColonyToken'; -export { MotionCreator } from './MotionCreator'; export { VotingReputation, Vote } from './VotingReputation'; +export { TxCreator } from './TxCreator'; diff --git a/src/constants.ts b/src/constants.ts index b732ee21..da608016 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1 +1,36 @@ -export { ColonyRole, Extension, Id, Tokens } from '@colony/colony-js'; +export { ColonyRole, Extension, Id, Network, Tokens } from '@colony/colony-js'; + +export enum MetaTxBroadCasterEndpoint { + /** The metatransaction broadcaster endpoint on mainnet */ + Mainnet = '', + /** The metatransaction broadcaster endpoint on the Görli testnet */ + Goerli = '', + /** The metatransaction broadcaster endpointon Gnosis chain */ + Gnosis = 'https://xdai.colony.io/metatransaction/xdai', + /** The metatransaction broadcaster endpointon Gnosis chain (alias) */ + Xdai = 'https://xdai.colony.io/metatransaction/xdai', + /** The metatransaction broadcaster endpointaddress on Gnosis chain */ + XdaiQa = 'https://xdai.colony.io/metatransaction/xdai', + /** The metatransaction broadcaster endpoint for a custom network */ + Custom = '', +} + +// TODO: Consider moving this to the metadata-parser +export enum TeamColor { + LightPink, + Pink, + Black, + EmeraldGreen, + Blue, + Yellow, + Red, + Green, + Periwinkle, + Gold, + Aqua, + BlueGrey, + Purple, + Orange, + Magenta, + PurpleGrey, +} diff --git a/src/events/ColonyEventManager.ts b/src/events/ColonyEventManager.ts index 5eaab656..197ed7d8 100644 --- a/src/events/ColonyEventManager.ts +++ b/src/events/ColonyEventManager.ts @@ -23,7 +23,8 @@ import { IpfsAdapter, IpfsMetadata, MetadataEvent, - MetadataEventValue, + MetadataType, + MetadataValue, } from '../ipfs'; /** Valid sources for Colony emitted events. Used to map the parsed event data */ @@ -45,7 +46,8 @@ export interface ColonyFilter extends Ethers6Filter { eventName: string; } -interface ColonyTopic { +/** A Colony specific topic that keeps track of which contract it belongs to */ +export interface ColonyTopic { eventSource: keyof EventSources; eventName: string; topic: string; @@ -63,11 +65,10 @@ export interface ColonyMultiFilter { } /** An Event that came from a contract within the Colony Network */ -export interface ColonyEvent - extends ColonyFilter { +export interface ColonyEvent extends ColonyFilter { data: Result; transactionHash: string; - getMetadata?: () => Promise>; + getMetadata?: () => Promise>; } /** Additional options for the [[ColonyEventManager]] */ @@ -95,7 +96,7 @@ export class ColonyEventManager { * Create a new ColonyEvents instance * * @remarks - * As opposed to the [[ColonyNetwork.ColonyNetwork]] class, this constructor _needs_ an _ethers_ JsonRpcProvider (or a subclass of it) as it's + * As opposed to the [[ColonyNetwork]] class, this constructor _needs_ an _ethers_ JsonRpcProvider (or a subclass of it) as it's * the only provider that supports topic filtering by multiple addresses * * @param provider An _ethers_ `JsonRpcProvider` @@ -151,7 +152,7 @@ export class ColonyEventManager { * Gets events for an individual filter and automatically parses the data if possible * * @example - * Retrieve and parse all `DomainAdded` events for a specific [[ColonyNetwork.Colony]] contract + * Retrieve and parse all `DomainAdded` events for a specific [[Colony]] contract * ```typescript * const domainAdded = colonyEvents.createFilter( * colonyEvents.eventSources.Colony, @@ -164,12 +165,12 @@ export class ColonyEventManager { * })(); * ``` * - * @param filter A [[ColonyFilter]]. [[ColonyMultiFilters]] will not work + * @param filter A [[ColonyFilter]]. [[ColonyMultiFilter]]s will not work * @returns An array of [[ColonyEvent]]s */ - async getEvents( + async getEvents( filter: ColonyFilter, - ): Promise>> { + ): Promise>> { const logs = await getLogs(filter, this.provider); return logs @@ -187,9 +188,9 @@ export class ColonyEventManager { transactionHash: log.transactionHash, getMetadata: async () => { return this.ipfs.getMetadataForEvent( - eventName as MetadataEvent, + eventName as MetadataEvent, data.metadata, - ); + ) as MetadataValue; }, }; } @@ -212,8 +213,8 @@ export class ColonyEventManager { * `fromBlock` and `toBlock` properties of the indivdual filters will be ignored * * @example - * Retrieve and parse all `DomainAdded` and `DomainMetadata` events for a specific [[ColonyNetwork.Colony]] contract. - * Note that we're using [[ColonyEvents.createMultiFilter]] here. The two `colonyAddress`es could also be different + * Retrieve and parse all `DomainAdded` and `DomainMetadata` events for a specific [[ColonyNetwork]] contract. + * Note that we're using [[createMultiFilter]] here. The two `colonyAddress`es could also be different * * ```typescript * const domainAdded = colonyEvents.createMultiFilter( @@ -233,16 +234,16 @@ export class ColonyEventManager { * })(); * ``` * - * @param filters An array of [[ColonyMultiFilter]]s. Normal [[ColonyFilters]] will not work + * @param filters An array of [[ColonyMultiFilter]]s. Normal [[ColonyFilter]]s will not work * @param options You can define `fromBlock` and `toBlock` only once for all the filters given * @param options.fromBlock Starting block in which to look for this event - inclusive (default: 'latest') * @param options.toBlock Ending block in which to look for this event - inclusive (default: 'latest') * @returns An array of [[ColonyEvent]]s */ - async getMultiEvents( + async getMultiEvents( filters: ColonyMultiFilter[], options: { fromBlock?: BlockTag; toBlock?: BlockTag } = {}, - ): Promise[]> { + ): Promise[]> { // Unique list of addresses const addresses = Array.from( new Set(filters.flatMap(({ address }) => address)), @@ -303,9 +304,9 @@ export class ColonyEventManager { ...colonyEvent, getMetadata: async () => { return this.ipfs.getMetadataForEvent( - eventName as MetadataEvent, + eventName as MetadataEvent, data.metadata, - ); + ) as MetadataValue; }, }; } @@ -325,7 +326,7 @@ export class ColonyEventManager { * We can do that as we do not have ambiguous events across our contracts, so we will always be able to find the right contract to parse the event data later on * * @example - * Filter for all `DomainAdded` events between block 21830000 and 21840000 (across all deployed [[ColonyNetwork.Colony]] contracts) + * Filter for all `DomainAdded` events between block 21830000 and 21840000 (across all deployed [[ColonyNetwork]] contracts) * ```typescript * const domainAdded = colonyEvents.createFilter( * colonyEvents.eventSources.Colony, @@ -398,7 +399,7 @@ export class ColonyEventManager { * We can do that as we do not have ambiguous events across our contracts, so we will always be able to find the right contract to parse the event data later on. Note that `ColonyMultiFilter` does not allow for params to be passed in. * * @example - * Filter for all `DomainAdded` events for a specific [[ColonyNetwork.Colony]] contract + * Filter for all `DomainAdded` events for a specific [[Colony]] contract * ```typescript * const domainAdded = colonyEvents.createFilter( * colonyEvents.eventSources.Colony, diff --git a/src/events/index.ts b/src/events/index.ts index 32c33ada..982fc02b 100644 --- a/src/events/index.ts +++ b/src/events/index.ts @@ -3,4 +3,13 @@ * @module events */ -export * from './ColonyEventManager'; +export { + ColonyEvent, + ColonyEventManager, + ColonyEventManagerOptions, + ColonyTopic, + ColonyFilter, + ColonyMultiFilter, + EventSource, + EventSources, +} from './ColonyEventManager'; diff --git a/src/index.ts b/src/index.ts index 2ddf6fd0..96d42949 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,15 @@ -export { MotionState, Tokens } from '@colony/colony-js'; - -export { - ColonyNetwork, - Colony, - ColonyToken, - MotionCreator, - VotingReputation, - Vote, -} from './ColonyNetwork'; -export { ColonyEventManager } from './events/ColonyEventManager'; +export { MotionState, NetworkClientOptions, Tokens } from '@colony/colony-js'; +export type { AnnotationMetadata } from '@colony/colony-event-metadata-parser'; +export * from './ColonyNetwork'; +export * from './events'; export * from './events'; - export * from './ipfs'; - export * from './constants'; - -export * from './types'; - export * from './utils'; + +export type { + Ethers6Filter, + Ethers6FilterByBlockHash, + ParsedLogTransactionReceipt, +} from './types'; diff --git a/src/ipfs/IpfsMetadata.ts b/src/ipfs/IpfsMetadata.ts index c56b8713..299b64ad 100644 --- a/src/ipfs/IpfsMetadata.ts +++ b/src/ipfs/IpfsMetadata.ts @@ -18,6 +18,8 @@ import { import IpfsAdapter from './IpfsAdapter'; import CloudflareReadonlyAdapter from './CloudflareReadonlyAdapter'; +export { MetadataType } from '@colony/colony-event-metadata-parser'; + const fetchRetry = wrapFetch(fetch, { headers: { Accept: 'application/json', @@ -27,12 +29,12 @@ const fetchRetry = wrapFetch(fetch, { retryDelay: 5000, }); -const IPFS_METADATA_EVENTS = { - 'Annotation(address,bytes32,string)': MetadataType.Annotation, - 'ColonyMetadata(address,string)': MetadataType.Colony, - 'DomainMetadata(address,uint256,string)': MetadataType.Domain, - Decision: MetadataType.Decision, - MISC: MetadataType.Misc, +export const IPFS_METADATA_EVENTS = { + [MetadataType.Annotation]: 'Annotation(address,bytes32,string)', + [MetadataType.Colony]: 'ColonyMetadata(address,string)', + [MetadataType.Decision]: '', + [MetadataType.Domain]: 'DomainMetadata(address,uint256,string)', + [MetadataType.Misc]: '', } as const; const IPFS_METADATA_PARSERS = { @@ -41,6 +43,9 @@ const IPFS_METADATA_PARSERS = { [MetadataType.Domain]: getDomainMetadataFromResponse, [MetadataType.Decision]: getDecisionDetailsFromResponse, [MetadataType.Misc]: getMiscDataFromResponse, + None: () => { + // Deliberately empty + }, } as const; const IPFS_METADATA_ENCODERS = { @@ -51,10 +56,8 @@ const IPFS_METADATA_ENCODERS = { [MetadataType.Misc]: getStringForMetadataMisc, } as const; -export type MetadataEvent = keyof typeof IPFS_METADATA_EVENTS; -export type MetadataEventValue = ReturnType< - typeof IPFS_METADATA_PARSERS[typeof IPFS_METADATA_EVENTS[K]] ->; +export type MetadataEvent = + typeof IPFS_METADATA_EVENTS[K]; export type MetadataValue = ReturnType< typeof IPFS_METADATA_PARSERS[K] >; @@ -68,7 +71,7 @@ export type MetadataValue = ReturnType< * @internal * */ -class IpfsMetadata { +export class IpfsMetadata { private adapter: IpfsAdapter; constructor(adapter?: IpfsAdapter) { @@ -100,17 +103,23 @@ class IpfsMetadata { } } - async getMetadataForEvent( + async getMetadataForEvent>( eventName: E, cid: string, - ): Promise> { + ): Promise> { const url = this.adapter.getIpfsUrl(cid); const res = await fetchRetry(url); try { const data = await res.text(); - return IPFS_METADATA_PARSERS[IPFS_METADATA_EVENTS[eventName]]( - data, - ) as MetadataEventValue; + const entry = Object.entries(IPFS_METADATA_EVENTS).find( + ([, value]) => value === eventName, + ); + if (!entry) { + throw new Error(`Not a valid MetadataEvent: ${eventName}`); + } + const metadataType = entry[0] as T; + + return IPFS_METADATA_PARSERS[metadataType](data) as MetadataValue; } catch (e) { throw new Error( `Could not parse IPFS metadata. Original error: ${ @@ -146,5 +155,3 @@ class IpfsMetadata { return this.adapter.uploadJson(str); } } - -export default IpfsMetadata; diff --git a/src/ipfs/PinataAdapter.ts b/src/ipfs/PinataAdapter.ts index fcf6e699..23d106aa 100644 --- a/src/ipfs/PinataAdapter.ts +++ b/src/ipfs/PinataAdapter.ts @@ -1,9 +1,14 @@ import type IpfsAdapter from './IpfsAdapter'; +const COLONY_IPFS_PINATA_TOKEN = + typeof global != 'undefined' && global.process + ? global.process.env.COLONY_IPFS_PINATA_TOKEN + : undefined; + /** * PinataAdapter * - * A Colony SDK IPFS adapter for Pinata (https://pinata.cloud). In order to use this, sign up for Pinata (if you haven't already) and generate a token. Then either supply this token when instantiating the class (example below) or provide it via the environment variable `COLONY_IPFS_PINATA_TOKEN`. Then provide an instance of this class to the [[ColonyNetwork]] or [[ColonyEventManger]] classes (depending on your needs). + * A Colony SDK IPFS adapter for Pinata (https://pinata.cloud). In order to use this, sign up for Pinata (if you haven't already) and generate a token. Then either supply this token when instantiating the class (example below) or provide it via the environment variable `COLONY_IPFS_PINATA_TOKEN` (when using NodeJS). Then provide an instance of this class to the [[ColonyNetwork]] or [[ColonyEventManager]] classes (depending on your needs). * * @remarks DO NOT CHECK IN YOUR PINATA TOKEN INTO VERSION CONTROL AND **DO NOT EMBED IT INTO YOUR FRONTEND BUNDLE**. * @@ -23,10 +28,10 @@ class PinataAdapter implements IpfsAdapter { name = 'PINATA'; constructor(pinataToken?: string) { - const token = pinataToken || process.env.COLONY_IPFS_PINATA_TOKEN; + const token = pinataToken || COLONY_IPFS_PINATA_TOKEN; if (!token) { throw new Error( - `Cannot find pinata token. Please supply it as an argument to the class or as "process.env.COLONY_IPFS_PINATA_TOKEN"`, + `Cannot find pinata token. Please supply it as an argument to the class or as "process.env.COLONY_IPFS_PINATA_TOKEN (in NodeJS)"`, ); } this.token = token; diff --git a/src/ipfs/index.ts b/src/ipfs/index.ts index f5969a22..c3866824 100644 --- a/src/ipfs/index.ts +++ b/src/ipfs/index.ts @@ -1,7 +1,7 @@ export { - default as IpfsMetadata, + IpfsMetadata, MetadataEvent, - MetadataEventValue, + MetadataType, MetadataValue, } from './IpfsMetadata'; diff --git a/src/types.ts b/src/types.ts index 6286b9a3..8559a8b9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,24 +1,44 @@ import type { Filter, FilterByBlockHash, + TransactionReceipt, } from '@ethersproject/abstract-provider'; +import type { LogDescription } from '@ethersproject/abi'; -/** Ethers 6 supports mulitple addresses in a filter. Until then we have this */ +/** + * Custom Transaction receipt for when we manually have to parse logs (metatransactions) + */ +export interface ParsedLogTransactionReceipt extends TransactionReceipt { + parsedLogs: LogDescription[]; +} + +/** + * Ethers 6 supports mulitple addresses in a filter. Until then we have this + */ export interface Ethers6Filter extends Omit { address?: string | string[]; } -/** Ethers 6 supports mulitple addresses in a filter. Until then we have this */ +/** + * Ethers 6 supports mulitple addresses in a filter. Until then we have this + */ export interface Ethers6FilterByBlockHash extends Omit { address?: string | string[]; } -/** @internal +/** + * @internal + * Type helper. Extracts all parameter types + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type Parameters = F extends (...rest: infer R) => any ? R : never; + +/** + * @internal * Type helper. Extracts all parameter types except the first two * Mostly used for removing the proof params from contract transaction methods */ -/* eslint-disable @typescript-eslint/no-explicit-any */ export type ParametersFrom2 = F extends ( arg0: any, arg1: any, @@ -27,3 +47,10 @@ export type ParametersFrom2 = F extends ( ? R : never; /* eslint-enable @typescript-eslint/no-explicit-any */ + +/** + * @internal + * Type helper. Forces TypeScript to expand the contents in the docs + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export type Expand = {} & { [P in keyof T]: T[P] }; diff --git a/src/utils.ts b/src/utils.ts index 49ea04af..7bd8891d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,37 +3,51 @@ import type { Log } from '@ethersproject/abstract-provider'; import type { JsonRpcProvider } from '@ethersproject/providers'; import type { Interface } from '@ethersproject/abi'; -import { Ethers6Filter, Ethers6FilterByBlockHash } from './types'; +import { + Ethers6Filter, + Ethers6FilterByBlockHash, + ParsedLogTransactionReceipt, +} from './types'; /** Extract event args from a contract receipt */ export const extractEvent = ( eventName: string, - receipt: ContractReceipt, + receipt: ContractReceipt | ParsedLogTransactionReceipt, ): T | undefined => { - if (receipt.events) { + if ('events' in receipt && receipt.events) { const event = receipt.events.find((ev) => ev.event === eventName); if (event?.args) { return event.args as ReadonlyArray & T; } + } else if ('parsedLogs' in receipt && receipt.parsedLogs) { + const event = receipt.parsedLogs.find((ev) => ev.name === eventName); + if (event?.args) { + return event.args as ReadonlyArray & T; + } } return undefined; }; -/** Manually extract an event from logs (e.g. if emitting contract is a different one than the calling contract) */ -export const extractEventFromLogs = ( +/** Manually extract an event using the interface (e.g. if emitting contract is a different one than the calling contract) */ +export const extractCustomEvent = ( eventName: string, - receipt: ContractReceipt, + receipt: ContractReceipt | ParsedLogTransactionReceipt, iface: Interface, ): T | undefined => { - if (!receipt.events || !receipt.events.length) { - return undefined; + let events: { data: string; topics: string[] }[]; + if ('events' in receipt && receipt.events) { + events = receipt.events; + } else if ('logs' in receipt && receipt.logs) { + events = receipt.logs; + } else { + events = []; } - for (let i = 0; i < receipt.events.length; i += 1) { + for (let i = 0; i < events.length; i += 1) { try { return iface.decodeEventLog( eventName, - receipt.events[i].data, - receipt.events[i].topics, + events[i].data, + events[i].topics, ) as ReadonlyArray & T; } catch (e) { // ignore