Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API Refactor: Transaction #224

Merged
merged 6 commits into from
Aug 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/connect-core/src/connections/ConnectorJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */

import { AppFilters, Network, SubscriptionHandler } from '@aragon/connect-types'

import { ConnectionContext } from '../types'
import IOrganizationConnector from './IOrganizationConnector'
import App from '../entities/App'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AppFilters, Network, SubscriptionHandler } from '@aragon/connect-types'

import { ConnectionContext } from '../types'
import App from '../entities/App'
import Organization from '../entities/Organization'
Expand Down
30 changes: 27 additions & 3 deletions packages/connect-core/src/entities/App.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { ethers } from 'ethers'

import Organization from './Organization'
import Repo from './Repo'
import Role from './Role'
import {
Abi,
AppIntent,
AppMethod,
AragonArtifact,
AragonManifest,
Metadata,
Expand Down Expand Up @@ -59,6 +61,28 @@ export default class App {
return this.organization.connection.orgConnector
}

contract(): ethers.Contract {
if (!this.abi) {
throw new Error(
`No ABI specified in app for ${this.address}. Make sure the metada for the app is available`
)
}
return new ethers.Contract(
this.address,
this.abi,
this.organization.connection.ethersProvider
)
}

interface(): ethers.utils.Interface {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A helper to access an Ethers interface object from the app entities.

if (!this.abi) {
throw new Error(
`No ABI specified in app for ${this.address}. Make sure the metada for the app is available`
)
}
return new ethers.utils.Interface(this.abi)
}

async repo(): Promise<Repo> {
return this.orgConnector().repoForApp(this.organization, this.address)
}
Expand All @@ -79,11 +103,11 @@ export default class App {
return this.artifact.abi
}

get intents(): AppIntent[] {
get methods(): AppMethod[] {
return this.artifact.functions
}

get deprecatedIntents(): { [version: string]: AppIntent[] } {
get deprecatedMethods(): { [version: string]: AppMethod[] } {
return this.artifact.deprecatedFunctions
}

Expand Down
77 changes: 77 additions & 0 deletions packages/connect-core/src/entities/ForwardingPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { ethers } from 'ethers'

import App from './App'
import Transaction from './Transaction'
import {
AppOrAddress,
ForwardingPathData,
ForwardingPathDescriptionData,
PostProcessDescription,
} from '../types'
import { describeForwardingPath } from '../utils/description'

export default class ForwardingPath {
#provider: ethers.providers.Provider
readonly apps: App[]
readonly destination: App
readonly transactions: Transaction[]

constructor(data: ForwardingPathData, provider: ethers.providers.Provider) {
this.#provider = provider
this.apps = data.apps
this.destination = data.destination
this.transactions = data.transactions
}

// Lets consumers pass a callback to sign any number of transactions.
// This is similar to calling transactions() and using a loop, but shorter.
// It returns the value returned by the library, usually a transaction receipt.
async sign<Receipt>(
callback: (tx: Transaction) => Promise<Receipt>
): Promise<Receipt[]> {
return Promise.all(this.transactions.map(async (tx) => await callback(tx)))
}

// Return a description of the forwarding path, to be rendered.
async describe(): Promise<ForwardingPathDescription> {
// TODO: Make sure we are safe to only provide the apps on the path here
return describeForwardingPath(this.transactions, this.apps, this.#provider)
}

// Return a description of the forwarding path, as text.
// Shorthand for .describe().toString()
toString(): string {
return this.describe().toString()
}
}

type ForwardingPathDescriptionTreeEntry =
| AppOrAddress
| [AppOrAddress, ForwardingPathDescriptionTreeEntry[]]

export type ForwardingPathDescriptionTree = ForwardingPathDescriptionTreeEntry[]

export class ForwardingPathDescription {
readonly apps: App[]
readonly describeSteps: PostProcessDescription[]

constructor(data: ForwardingPathDescriptionData) {
this.apps = data.apps
this.describeSteps = data.describeSteps
}

// Return a tree that can get used to render the path.
tree(): ForwardingPathDescriptionTree {
// TODO:
return []
}

// Renders the forwarding path description as text
toString(): string {
return this.tree().toString()
}

// TBD: a utility that makes it easy to render the tree,
// e.g. as a nested list in HTML or React.
reduce(callback: Function): any {}
}
81 changes: 81 additions & 0 deletions packages/connect-core/src/entities/Intent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { ethers } from 'ethers'

import Organization from './Organization'
import Transaction from './Transaction'
import TransactionPath from '../transactions/TransactionPath'
import { PathOptions, IntentData } from '../types'
import { calculateTransactionPath } from '../utils/path/calculatePath'

export default class Intent {
#org: Organization
#provider: ethers.providers.Provider
readonly appAddress: string
readonly functionName: string
readonly functionArgs: any[]

constructor(
data: IntentData,
org: Organization,
provider: ethers.providers.Provider
) {
this.#org = org
this.#provider = provider
this.appAddress = data.appAddress
this.functionName = data.functionName
this.functionArgs = data.functionArgs
}

// Retrieve a single forwarding path. Defaults to the shortest one.
async path({ actAs, path }: PathOptions): Promise<TransactionPath> {
const apps = await this.#org.apps()

// Get the destination app
const destination = apps.find((app) => app.address == this.appAddress)
if (!destination) {
throw new Error(
`Destination (${this.appAddress}) is not an installed app`
)
}

const transactions = await calculateTransactionPath(
actAs,
destination,
this.functionName,
this.functionArgs,
apps,
this.#provider
)

return new TransactionPath({
apps: apps.filter((app) =>
transactions
.map((tx) => tx.to)
.some((address) => address === app.address)
),
destination,
transactions,
})
}

// Retrieve the different possible forwarding paths.
async paths({ actAs, path }: PathOptions): Promise<TransactionPath[]> {
// TODO: support logic to calculate multiple Forwarding paths
return [await this.path({ actAs, path })]
}

// A list of transaction requests ready to get signed.
async transactions(options: PathOptions): Promise<Transaction[]> {
return (await this.path(options)).transactions
}

// Lets consumers pass a callback to sign any number of transactions.
// This is similar to calling transactions() and using a loop, but shorter.
// It returns the value returned by the library, usually a transaction receipt.
async sign<Receipt>(
callback: (tx: Transaction) => Promise<Receipt>,
options: PathOptions
): Promise<Receipt[]> {
const txs = await this.transactions(options)
return Promise.all(txs.map(async (tx) => await callback(tx)))
}
}
17 changes: 13 additions & 4 deletions packages/connect-core/src/entities/Organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import {
AppFiltersParam,
SubscriptionHandler,
} from '@aragon/connect-types'

import { ConnectionContext } from '../types'
import TransactionIntent from '../transactions/TransactionIntent'
import { decodeForwardingPath } from '../utils/description'
import { toArrayEntry } from '../utils/misc'
import App from './App'
import Intent from './Intent'
import { ForwardingPathDescription } from './ForwardingPath'
import Permission from './Permission'

// TODO
Expand Down Expand Up @@ -126,11 +129,17 @@ export default class Organization {
appAddress: Address,
functionName: string,
functionArgs: any[]
): TransactionIntent {
return new TransactionIntent(
{ contractAddress: appAddress, functionName, functionArgs },
): Intent {
return new Intent(
{ appAddress, functionName, functionArgs },
this,
this.connection.ethersProvider
)
}

//////// DESCRIPTIONS /////////

async describeScript(script: string): Promise<ForwardingPathDescription> {
return decodeForwardingPath(script, this.apps(), this.connection)
}
}
15 changes: 15 additions & 0 deletions packages/connect-core/src/entities/Transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Address } from '@aragon/connect-types'

import { TransactionData } from '../types'

export default class Transaction {
readonly data: string
readonly from: Address
readonly to: Address

constructor(data: TransactionData) {
this.data = data.data
this.from = data.from
this.to = data.to
}
}
1 change: 1 addition & 0 deletions packages/connect-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
Networkish,
SubscriptionHandler,
} from '@aragon/connect-types'

export { default as IOrganizationConnector } from './connections/IOrganizationConnector'
export {
default as ConnectorJson,
Expand Down
74 changes: 0 additions & 74 deletions packages/connect-core/src/transactions/TransactionIntent.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/connect-core/src/transactions/TransactionPath.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import TransactionRequest from './TransactionRequest'
import App from '../entities/App'
import Transaction from '../entities/Transaction'
import { TransactionPathData } from '../types'

export default class TransactionPath {
readonly apps!: App[]
readonly destination!: App
readonly forwardingFeePretransaction?: TransactionRequest
readonly transactions!: TransactionRequest[]
readonly forwardingFeePretransaction?: Transaction
readonly transactions!: Transaction[]

constructor(data: TransactionPathData) {
this.apps = data.apps
Expand Down
Loading