diff --git a/app/src/api/lit.ts b/app/src/api/lit.ts index a0952d8a3..aa9ebf03d 100644 --- a/app/src/api/lit.ts +++ b/app/src/api/lit.ts @@ -1,6 +1,9 @@ -import * as LIT from 'types/generated/lit-sessions_pb'; +import * as ACCOUNT from 'types/generated/lit-accounts_pb'; +import * as SESSION from 'types/generated/lit-sessions_pb'; +import { Accounts } from 'types/generated/lit-accounts_pb_service'; import { Sessions } from 'types/generated/lit-sessions_pb_service'; import { b64 } from 'util/strings'; +import { MAX_DATE } from 'util/constants'; import BaseApi from './base'; import GrpcClient from './grpc'; @@ -16,24 +19,46 @@ class LitApi extends BaseApi { this._grpc = grpc; } + /** + * call the Lit `CreateAccount` RPC and return the response + */ + async createAccount( + accountBalance: number, + expirationDate: Date, + ): Promise { + const req = new ACCOUNT.CreateAccountRequest(); + req.setAccountBalance(accountBalance.toString()); + + if (expirationDate === MAX_DATE) { + req.setExpirationDate('0'); + } else { + req.setExpirationDate(Math.floor(expirationDate.getTime() / 1000).toString()); + } + + const res = await this._grpc.request(Accounts.CreateAccount, req, this._meta); + return res.toObject(); + } + /** * call the Lit `AddSession` RPC and return the response */ async addSession( label: string, - sessionType: LIT.SessionTypeMap[keyof LIT.SessionTypeMap], + sessionType: SESSION.SessionTypeMap[keyof SESSION.SessionTypeMap], expiry: Date, mailboxServerAddr: string, devServer: boolean, - macaroonCustomPermissions: Array, - ): Promise { - const req = new LIT.AddSessionRequest(); + macaroonCustomPermissions: Array, + accountId: string, + ): Promise { + const req = new SESSION.AddSessionRequest(); req.setLabel(label); req.setSessionType(sessionType); req.setExpiryTimestampSeconds(Math.floor(expiry.getTime() / 1000).toString()); req.setMailboxServerAddr(mailboxServerAddr); req.setDevServer(devServer); req.setMacaroonCustomPermissionsList(macaroonCustomPermissions); + req.setAccountId(accountId); const res = await this._grpc.request(Sessions.AddSession, req, this._meta); return res.toObject(); @@ -42,8 +67,8 @@ class LitApi extends BaseApi { /** * call the Lit `ListSessions` RPC and return the response */ - async listSessions(): Promise { - const req = new LIT.ListSessionsRequest(); + async listSessions(): Promise { + const req = new SESSION.ListSessionsRequest(); const res = await this._grpc.request(Sessions.ListSessions, req, this._meta); return res.toObject(); } @@ -53,8 +78,8 @@ class LitApi extends BaseApi { */ async revokeSession( localPublicKey: string, - ): Promise { - const req = new LIT.RevokeSessionRequest(); + ): Promise { + const req = new SESSION.RevokeSessionRequest(); req.setLocalPublicKey(b64(localPublicKey)); const res = await this._grpc.request(Sessions.RevokeSession, req, this._meta); return res.toObject(); diff --git a/app/src/components/common/FormInputNumber.tsx b/app/src/components/common/FormInputNumber.tsx index bf9edc9a7..5c834c4dd 100644 --- a/app/src/components/common/FormInputNumber.tsx +++ b/app/src/components/common/FormInputNumber.tsx @@ -7,6 +7,7 @@ interface Props { value?: number; extra?: ReactNode; placeholder?: string; + className?: string; onChange: (value: number) => void; } @@ -15,6 +16,7 @@ const FormInputNumber: React.FC = ({ value, extra, placeholder, + className, onChange, }) => { const handleChange = useCallback( @@ -35,6 +37,7 @@ const FormInputNumber: React.FC = ({ return ( props.theme.colors.lightningNavy}; + } + `, FormDate: styled(FormDate)` input { font-family: ${props => props.theme.fonts.open.regular}; @@ -122,6 +128,7 @@ const CustomSessionPage: React.FC = () => { Permission, FormSelect, FormInput, + FormInputNumber, FormDate, Small, Button, @@ -199,6 +206,14 @@ const CustomSessionPage: React.FC = () => { {l('paymentsDesc')} + + {l('custodial')} + {l('custodialDesc')} + + { + {addSessionView.permissionType === 'custodial' && ( + + + + sats} + /> + + )} +
{l('permView')} diff --git a/app/src/components/connect/SessionRow.tsx b/app/src/components/connect/SessionRow.tsx index 51987fdd2..193fd5fd9 100644 --- a/app/src/components/connect/SessionRow.tsx +++ b/app/src/components/connect/SessionRow.tsx @@ -7,6 +7,7 @@ import { Session } from 'store/models'; import { BoltOutlined, Close, Column, Copy, QRCode, Row } from 'components/base'; import SortableHeader from 'components/common/SortableHeader'; import Tip from 'components/common/Tip'; +import * as LIT from 'types/generated/lit-sessions_pb'; import QRCodeModal from './QRCodeModal'; /** @@ -151,6 +152,18 @@ const SessionRow: React.FC = ({ session, style }) => { + ) : session.type === LIT.SessionType.TYPE_MACAROON_ACCOUNT ? ( + <> + + + + + + + + + + ) : ( <> diff --git a/app/src/i18n/locales/en-US.json b/app/src/i18n/locales/en-US.json index e37602a3b..a6bfb67e1 100644 --- a/app/src/i18n/locales/en-US.json +++ b/app/src/i18n/locales/en-US.json @@ -53,9 +53,12 @@ "cmps.connect.CustomSessionPage.liquidityDesc": "User can only set fees, use Loop, and use Pool.", "cmps.connect.CustomSessionPage.payments": "Payments Manager", "cmps.connect.CustomSessionPage.paymentsDesc": "User can only send and receive payments.", + "cmps.connect.CustomSessionPage.custodial": "Custodial Account", + "cmps.connect.CustomSessionPage.custodialDesc": "Create a custodial off-chain account for your node.", "cmps.connect.CustomSessionPage.custom": "Custom", "cmps.connect.CustomSessionPage.customDesc": "Create a session with fully custom permissions.", "cmps.connect.CustomSessionPage.permissions": "Permissions", + "cmps.connect.CustomSessionPage.addBalance": "Add Balance", "cmps.connect.CustomSessionPage.permView": "View Activity", "cmps.connect.CustomSessionPage.permViewDesc": "See node history and activity.", "cmps.connect.CustomSessionPage.permOpen": "Open Channel", @@ -86,6 +89,7 @@ "cmps.connect.SessionRow.pairTerminal": "Pair with Lightning Terminal", "cmps.connect.SessionRow.generateQR": "Generate QR Code", "cmps.connect.SessionRow.revoke": "Revoke Session", + "cmps.connect.SessionRow.pairCustodial": "Custodial accounts can not be used with Terminal", "cmps.connect.QRCodeModal.title": "LNC QR", "cmps.connect.QRCodeModal.desc": "Scan to connect to Terminal from your mobile phone.", "cmps.home.HomePage.pageTitle": "Home", diff --git a/app/src/store/models/session.ts b/app/src/store/models/session.ts index 2013a2b38..f1840d586 100644 --- a/app/src/store/models/session.ts +++ b/app/src/store/models/session.ts @@ -59,6 +59,8 @@ export default class Session { return 'Admin'; case LIT.SessionType.TYPE_MACAROON_CUSTOM: return 'Custom'; + case LIT.SessionType.TYPE_MACAROON_ACCOUNT: + return 'Custodial'; case LIT.SessionType.TYPE_UI_PASSWORD: return 'LiT UI Password'; } @@ -96,7 +98,7 @@ export default class Session { /** The HEX encoded pairing secret mnemonic and mailbox server address */ get encodedPairingData() { - const data = `${this.pairingSecretMnemonic}||${this.mailboxServerAddr}`; + const data = `${this.pairingSecretMnemonic}||${this.mailboxServerAddr}||${this.typeLabel}`; return Buffer.from(data, 'ascii').toString('base64'); } diff --git a/app/src/store/stores/sessionStore.ts b/app/src/store/stores/sessionStore.ts index a12ac570c..10aecd13f 100644 --- a/app/src/store/stores/sessionStore.ts +++ b/app/src/store/stores/sessionStore.ts @@ -104,6 +104,7 @@ export default class SessionStore { copy = false, proxy?: string, customPermissions?: LIT.MacaroonPermission[], + accountId?: string, ) { try { this._store.log.info(`submitting session with label ${label}`, { @@ -119,6 +120,7 @@ export default class SessionStore { proxy || this.proxyServer, !IS_PROD, customPermissions || [], + accountId || '', ); // fetch all sessions to update the store's state diff --git a/app/src/store/views/addSessionView.ts b/app/src/store/views/addSessionView.ts index c3ba5571d..86660276e 100644 --- a/app/src/store/views/addSessionView.ts +++ b/app/src/store/views/addSessionView.ts @@ -7,7 +7,7 @@ export default class AddSessionView { private _store: Store; label = ''; - permissionType = 'admin'; // Expected values: admin | read-only | custom | liquidity | payments + permissionType = 'admin'; // Expected values: admin | read-only | custodial | custom | liquidity | payments editing = false; permissions: { [key: string]: boolean } = { openChannel: false, @@ -30,6 +30,7 @@ export default class AddSessionView { expirationDate = ''; showAdvanced = false; proxy = ''; + custodialBalance = 0; constructor(store: Store) { makeAutoObservable( @@ -52,6 +53,8 @@ export default class AddSessionView { return LIT.SessionType.TYPE_MACAROON_ADMIN; } else if (this.permissionType === 'read-only') { return LIT.SessionType.TYPE_MACAROON_READONLY; + } else if (this.permissionType === 'custodial') { + return LIT.SessionType.TYPE_MACAROON_ACCOUNT; } return LIT.SessionType.TYPE_MACAROON_CUSTOM; @@ -125,6 +128,10 @@ export default class AddSessionView { this.proxy = proxy; } + setCustodialBalance(balance: number) { + this.custodialBalance = balance; + } + setPermissionType(permissionType: string) { this.permissionType = permissionType; @@ -150,6 +157,12 @@ export default class AddSessionView { this.permissions.receive = true; break; + case 'custodial': + this.setAllPermissions(false); + this.permissions.send = true; + this.permissions.receive = true; + break; + case 'custom': // We don't need to change anything, let the user customize permissions how they want break; @@ -203,12 +216,24 @@ export default class AddSessionView { async handleCustomSubmit() { let label = this.label; + let accountId = ''; // Automatically generate human friendly labels for custom sessions if (label === '') { label = `My ${this.permissionType} session`; } + if (this.permissionType === 'custodial') { + const custodialAccountId = await this.registerCustodialAccount(); + + // Return immediately to prevent a session being created when there is an error creating the custodial account + if (!custodialAccountId) { + return; + } + + accountId = custodialAccountId; + } + const session = await this._store.sessionStore.addSession( label, this.sessionType, @@ -216,6 +241,7 @@ export default class AddSessionView { true, this.sessionProxy, this.getMacaroonPermissions, + accountId, ); if (session) { @@ -224,6 +250,21 @@ export default class AddSessionView { } } + async registerCustodialAccount(): Promise { + try { + const response = await this._store.api.lit.createAccount( + this.custodialBalance, + this.sessionDate, + ); + + if (response.account) { + return response.account.id; + } + } catch (error) { + this._store.appView.handleError(error, 'Unable to register custodial account'); + } + } + // // Private helper functions //