Giter Site home page Giter Site logo

pw-core's Introduction

pw-core: A Friendly and Powerful SDK for CKB dApps

pw-core is the front-end sdk of pw-sdk

Quick Start

Installation

You can install pw-core to your project with npm

# in your project root
$ npm install @lay2/pw-core --save

Or with yarn

# in your project root
$ yarn add @lay2/pw-core

Hello World

Let's see how to send CKB with pw-core.

import PWCore, {
  EthProvider,
  PwCollector,
  ChainID,
  Address,
  Amount,
  AddressType,
} from '@lay2/pw-core';

// insdie an async scope

const pwcore = await new PWCore('https://ckb-node-url').init(
  new EthProvider(), // a built-in Provider for Ethereum env.
  new PwCollector() // a custom Collector to retrive cells from cache server.
);

const txHash = await pwcore.send(
  new Address('0x26C5F390FF2033CbB44377361c63A3Dd2DE3121d', AddressType.eth),
  new Amount('100')
);

That's it! If CKB transaction (with Ethereum wallets, e.g. MetaMask) is the only thing you need, you can already start your integration with pw-core.

One Step Further

However, if you need more features, such as adding multiple outputs, setting data, or adding custom lock/type scripts, you can always implement you own builder extends the Builder class. If you have more requirements with retriving unspent cells, a custom cell collector based on Collector is a good choice. The same approach applies to Signer / Hasher / Provider. In fact, you will find that almost every aspect of buiding a transaction can be customized to meet your demands. This is because we have well encapsulated the transaction process as build -> sign -> send, and any kind of transaction can be created and sent given a builder and a signer. For example, the basic send method used in the Hello World example is implented like this:

// code from: https://github.com/lay2dev/pw-core/blob/master/src/core.ts#L80

import { transformers } from 'ckb-js-toolkit'
import { Address, Amount } from './models'
import { SimpleBuilder } from './builders'
import { EthSigner } from './signers'

async send(address: Address, amount: Amount, feeRate?: number): Promise<string> {
  const simpleBuilder = new SimpleBuilder(address, amount, feeRate);
  const = new EthSigner(address.addressString);
  return this.sendTransaction(simpleBuilder, ethSigner);
}

async sendTransaction(builder: Builder, signer: Signer): Promise<string> {
  return this.rpc.send_transaction(
    transformers.TransformTransaction(
      await signer.sign((await builder.build()).validate())
    )
  );
}

Finally, here is an example project which shows how to implement custom classes to achieve certain features. The SDCollector can collect unspent cells with a ckb-indexer, while the SDBuilder can build transactions for creating / updating / deleting cells. More over, the built-in EthProvider and EthSigner (along with Keccak256Hasher) are used to make this dApp runnable in Ethereum enviromment (such as MetaMask).

Highlights

  • Concise API

    We only provide two types of interfaces, one is the most commonly used and the other is fully customizable. For example, you can use pwcore.send to just send some CKB, and use pwcore.sendTransaction to send any type of transactions. More over, you can even use pwcore.rpc to get direct access to ckb rpc calls, which enables abilities far beyond sending transactions.

  • Thoughtful Models

    Talk is cheap, let's show some code.

    /* ------ Address ------ */
    
    /* create an Address instance from a CKB address */
    const ckbAddress = new Address('ckb1qyqdmeuqrsrnm7e5vnrmruzmsp4m9wacf6vsxasryq', AddressType.ckb);
    
    /* create an Address instance from an Ethereum address */
    const ethAddress = new Address('0x308f27c8595b2ee9e6a5faa875b4c1f9de6b679a', AddressType.eth);
    
    /* get the original address string */
    console.log('ckb: ', ckbAddress.addressString)  
    console.log('eth: ', ethAddress.addressString)
    // ckb: ckb1qyqdmeuqrsrnm7e5vnrmruzmsp4m9wacf6vsxasryq
    // eth: 0x308f27c8595b2ee9e6a5faa875b4c1f9de6b679a
    
    /* get the corresponding CKB address */
    console.log('ckb: ', ckbAddress.toCKBAddress())
    console.log('eth: ', ethAddress.toCKBAddress())
    // ckb: ckb1qyqdmeuqrsrnm7e5vnrmruzmsp4m9wacf6vsxasryq
    // eth: ckt1q3vvtay34wndv9nckl8hah6fzzcltcqwcrx79apwp2a5lkd07fdxxvy0yly9jkewa8n2t74gwk6vr7w7ddne5jrkf6c
    
    /* get the corresponding lock script hash (with the toHash method of class Script) */
    console.log('ckb: ', ethAddress.toLockScript().toHash())
    console.log('eth: ', ethAddress.toLockScript().toHash())
    // ckb: 0xe9e412caf497c69e9612d305be13f9173752b9e75bc5a9b6d1ca51eb38d07d59
    // eth: 0x0963476f28975bf93da673cd2442bd69c4b2d4e720af5a67ecece8a03b8926b5
    
    /* check if the address is an ACP address */
    console.log('ckb: ', ckbAddress.isAcp())
    console.log('eth: ', ethAddress.isAcp())
    // false
    // true
    
    // get the minimal CKB amount (an Amount instance) you can transfer to this address
    console.log('ckb: ', ckbAddress.minPaymentAmount().toString() + ' CKB')
    console.log('eth: ', ethAddress.minPaymentAmount().toString() + ' CKB')
    // 61 CKB
    // 0.00000001 CKB
    
    /* ------ Script ------ */
    
    const lockScript = addressCkb.toLockScript();
    const lockScriptHash = lockScript.toHash();
    const address1 = Address.fromLockScript(lockScript);
    const address2 = lockScript.toAddress();
    
    console.log(addressEth.toLockScript().sameWith(addressCkbFull.toLockScript()));
    //true
    
    /* ------ Amount ------ */
    
    const ckb100 = new Amount('100');
    const shannon100 = new Amount('100', AmountUnit.shannon);
    const usdt = new Amount('1234.5678', 6); // Assume usdt's decimals is 6
    
    /* format */
    
    console.log(`${ckb100.toString()} CKB is ${ckb100.toString(AmountUnit.shannon, {commify: true})} Shannon`);
    // 100 CKB is 1,000,000 Shannon
    
    console.log(`${shannon100.toString(AmountUnit.shannon)} Shannon is ${shannon100.toString()} CKB`)
    // 100 Shannon is 0.000001 CKB
    
    console.log(`${usdt.toString(6, {fixed: 2, commify: true})} USDT is rounded from ${usdt.toString(6, {pad: true})} USDT`);
    // 1,234.57 USDT is rounded from 1234.567800 USDT
    
    /* compare */
    
    console.log('100 CKB is greater than 100 Shannon: ', ckb100.gt(shannon100));
    console.log('100 CKB is less than 100 Shannon: ', ckb100.lt(shannon100));
    // 100 CKB is greater than 100 Shannon: true
    // 100 CKB is less than 100 Shannon: false
    
    console.log('100 Shannon is equal to 0.000001 CKB: ', shannon100.eq(new Amount('0.000001')));
    // 100 Shannon is equal to 0.000001 CKB: true
    
    /* calculate */
    
    console.log(`100 CKB + 100 Shannon = ${ckb100.add(shannon100).toString()} CKB`);
    console.log(`100 CKB - 100 Shannon = ${ckb100.sub(shannon100).toString()} CKB`);
    // 100 CKB + 100 Shannon = 100.000001 CKB
    // 100 CKB - 100 Shannon = 99.999999 CKB
    
    // Amount is assumed with unit, so if we want to perform multiplication or division, the best way is to convert the Amount instance to JSBI BigInt, and convert  back to Amount instance if necessary.
    const bn = JSBI.mul(ckb100.toBigInt(), JSBI.BigInt(10));
    const amount = new Amount(bn.toString())
    console.log(`100 CKB * 10 = ${amount.toString(AmountUnit.ckb, {commify: true})} CKB`);
    // 100 CKB * 10 = 1,000 CKB
    
    /* ------ Cell ------ */
    
    /* load from blockchain with a rpc instance and an outpoint */
    const cell1 = Cell.loadFromBlockchain(rpc, outpoint);
    
    /* convert rpc formated data to a Cell instance */
    const cell2 = Cell.fromRPC(rpcData);
    
    /* check how many capacity is occupied by scripts and data */
    const occupiedCapacity = cell1.occupiedCapacity();
    
    /* check if the cell's capacity is enough for the actual size */
    cell1.spaceCheck();
    
    /* check if the cell is empty (no data is stored) */
    cell2.isEmpty()
    
    /* adjust the capacity to the minimal value of this cell */
    cell2.resize();
    
    /* set / get amount of an sUDT cell */
    const cell3 = cell2.clone();
    cell3.setSUDTAmount(new Amount(100));
    console.log('sUDT amount: ', cell3.getSUDTAmount().toString());
    // sUDT amount: 100
    
    /* playing with data */
    cell1.setData('data');
    cell2.setHexData('0x64617461');
    console.log('data of cell 1: ', cell1.getData());
    console.log('data of cell 2: ', cell1.getData());
    console.log('hex data of cell 1: ', cell1.getHexData());
    // data of cell 1: data
    // data of cell 2: data
    // hex data of cell 1: 0x64617461
  • Simple and Clear Structure

CKB dApp development is mostly about manipulating cells. However, when we actually try to design a dApp, it turns out that we are always dealing with transactions. If you ever tried to build a CKB transaction, you'll definitely be impressed (or most likely, confused) by the bunch of fields. Questions may be asked like:

There are data structures like 'CellInput' and 'CellOutput', but where is 'Cell' ?

How to calculate the transaction fee? How to adjust the change output?

What kind of unit ( CKB or Shannon) and format (mostly hex string) to use?

Things are different with pw-core. Let's see the actual constructor of RawTransaction

// code from: https://github.com/lay2dev/pw-core/blob/master/src/models/raw-transaction.ts#L7

export class RawTransaction implements CKBModel {
  constructor(
    public inputCells: Cell[],
    public outputs: Cell[],
    public cellDeps: CellDep[] = [
      PWCore.config.defaultLock.cellDep,
      PWCore.config.pwLock.cellDep,
    ],
    public headerDeps: string[] = [],
    public readonly version: string = '0x0'
  ) {
    this.inputs = inputCells.map((i) => i.toCellInput());
    this.outputsData = this.outputs.map((o) => o.getHexData());
  }

  // ...
}

It's easy to findout that both inputs and outputs are Array of cells, and the low-level details and transformations are done silently. And yes, we have the 'Cell' structure.

  • Work With or Without pw-lock

    Although pw-core works closely with pw-lock (both projects are components of pw-sdk), you can still use the default blake2b lock or your own lock, as long as the corresponding builder, signer and hasher are implemented. In fact, the Blake2bHasher is already built-in, and a CkbSigner is very likely to be added in the near future. With the progress of pw-sdk, more algorithm be added to the built-in collections, such as Sha256Hasher and P256Signer.

API Document

You can find a detailed API Document Here.

Get Involved

Currently pw-core is still at a very early stage, and all kinds of suggestions and bug reports are very much welcomed. Feel free to open an issue, or join our Discord server to talk directly to us. And of couse, a star is very much appreciated :).

pw-core's People

Contributors

dependabot[bot] avatar johnz1019 avatar louzhixian avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

pw-core's Issues

New ACP hashes are not reflected in constants.

I'm submitting a ...

[x] bug report
[ ] feature request
[ ] question about the decisions made in the repository
[ ] question about how to use this project

Summary

There was an update to ACP and it was redeployed. The new hashes are not reflected in the PW-Core constants.

The new constants can be found here.

The blog post about the release can be found here.

Error when adding cutom feeRate ---> in pwCore.send(address, amount, {feeRate: 1100})

  • I'm submitting a ...
  • bug report
  • feature request
  • Summary
    The problem is in pwCore.send() method when adding {feeRate: 1100} (for example)
    Related to Builder.calcFee() --->
    const fee = (feeRate / FEE_BASE) * txSize;
    return new models_1.Amount(fee.toString(), models_1.AmountUnit.shannon);

Returned error: SyntaxError: Cannot convert 742.5000000000001 to a BigInt

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)

I think the problem is with fee.toString()
I'm using ${value} when creating a new Amount()
Need to be reproduced and re-checked

@sking789 @louzhixian

Update needed to support new CKB address format.

  • I'm submitting a ...
    [X] bug report
    [ ] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary
    A new address format has been introduced for CKB2021 that needs to be incorporated.

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)

The new format type is 0x00, and it deprecates all others including the short address.
https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0021-ckb-address-format/0021-ckb-address-format.md

Here is the Lumos code example: https://codesandbox.io/s/ckb-address-transformer-524gi?file=/src/App.js

Lines 17-19 show the functions to look at:

      const script = lumos.helpers.parseAddress(inputAddress);
      const deprecatedAddr = lumos.helpers.generateAddress(script);
      const newFullAddr = lumos.helpers.encodeToAddress(script);

Here is the address generation code in Lumos:
https://github.com/nervosnetwork/lumos/blob/ckb2021/packages/helpers/src/index.ts#L95-L154

Add ACP Lock to constants.

I'm submitting a ...

[ ] bug report
[x] feature request
[ ] question about the decisions made in the repository
[ ] question about how to use this project

Summary

Right now the chain specs with the constants contain:

  • daoType
  • sudtType
  • defaultLock
  • multiSigLock
  • pwLock
  • acpLockList

I believe it would be beneficial to also have acpLock added to this list, similar to defaultLock and pwLock. Right now it is listed in acpLockList, but this is a different format that does not include the CellDep, and since it is an array there is no way to distinguish the official ACP from other ACP locks.

More descriptive error for "The Receiver's address is not anyone-can-pay cell"

  • I'm submitting a ...
    [ ] bug report
    [X] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

If you send less than 61 CKB to non Anyone Can Pay lock cell you get:
"The Receiver's address is not anyone-can-pay cell"

This error could be more explanatory. IMO it should be something like this:

"You're sending less than Minimum Change Amount (61 CKB) and the Receiver's address is not Anyone-Can-Pay cell. Increase the amount you're sending or make sure Receiver's address is using ACP lock."

Add EthRawProvider

  • I'm submitting a ...
    [ ] bug report
    [X] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

So far existing RawProvider is using CKB default lock private key therefore if you want to test ETH in backend you can't. You have only EthProvider which is working in the browser. Adding EthRawProvider that works with an Ethereum private key and works on backend could be useful for automated/manual testing/backend scripts.

Valid address detected as invalid by `valid()`.

  • I'm submitting a ...
    [X] bug report
    [ ] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

The Address.valid() function returns false for the valid address ckb1qyp260h7pphjhlapmxqhrm7e0nmhujrqqmdq0vpvft.

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)
		const address1 = new Address('ckb1qyqx950al8f9xq07ex0j85mayujp66587zgqn4dwxa', AddressType.ckb);
		console.log(address1.valid()); // true
		const address2 = new Address('ckb1qyp260h7pphjhlapmxqhrm7e0nmhujrqqmdq0vpvft', AddressType.ckb);
		console.log(address2.valid()); // false

Both are valid addresses. The second address returns false.

Support Omni Lock in addition to PW-Lock.

  • I'm submitting a ...
    [ ] bug report
    [X] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary
    Add support for Omni Lock in addition to PW-Lock, and make Omni Lock the default.

Send method doesn't work because of wrong witnesses placeholders generated

  • I'm submitting a ...
    [x] bug report
    [ ] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

pwCore.send() method with RawProvider and privateKey that was just added in 0.4.0-alpha.6 seems to be broken. Especially the part that is failing with:

"... source: Inputs[0].Lock, cause: ValidationFailure(-31): the exit code is per script specific ...".

I'm using pwCore method (it abstracts away generating signature): https://gist.github.com/Kuzirashi/ddbcd1b56f6325077a12a27943f0d154

  • Other information

The problem seems to be length of witnessArgs passed to @lay2/pw-core/build/main/models/transaction.js. Broken (current state):

{
  witnessArgs: [
    {
      lock: '0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
      input_type: '',
      output_type: ''
    }
  ],
  witnesses: [ '0x' ]
}
// ... then witnesses placeholders are generated wrong because of this and it makes transaction fail \/

witnesses: [
    '0x5600000010000000560000005600000042000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
  ]

It should be instead:

{
  witnessArgs: [
    {
      lock: '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
      input_type: '',
      output_type: ''
    }
  ],
  witnesses: [ '0x' ]
}
// =>
witnesses: [
    '0x55000000100000005500000055000000410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'
  ]

From what I see this is the fix (transaction is committed to chain instead of error):
image

this.witnessArgs = witnessArgs.map(arg => ({ ...arg, lock: arg.lock.slice(0, arg.lock.length - 2) })); // FIX

Full tx structure:
image

After above fix I was able to successfully send from CKB address to ETH address on L1 using this script: https://gist.github.com/Kuzirashi/2f13e487645fd4d74503957429d21afc

Change Amount operations to better style

  • I'm submitting a ...
    [ ] bug report
    [x] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary
    Change Amount operations from static functions to instance functions, e.g.

from

a1 = Amount.ADD(a1, a2);

to

a1 = a1.add(a2);

EthProvider error handling

  • I'm submitting a ...
    [ ] bug report
    [x] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

  • Please, add handling for this error (after disconnection). And also it should be good if EthProvider could handle canceling of Metamask signing.

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)
    lay2coreError

When using pwcore.send() to send a transaction, the error "PoolRejectedTransactionByOutputs Validator: The transaction is rejected by OutputsValidator set in params[1]: well_known_scripts_only." is reported.

  • I'm submitting a ...
    [ ] bug report
    [ ] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

  • Other information (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.)

`sendSUDT` error: "Error: cell capacity 142 less than the min capacity X"

  • I'm submitting a ...
    [x] bug report
    [ ] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary

Example code: https://gist.github.com/Kuzirashi/5f69e4fac83959674d57b60e6775b815

Sending SUDT to Layer 2 Deposit Address results in following error:

/home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/node_modules/@lay2/pw-core/build/main/models/cell.js:57
            throw new Error(`cell capacity ${this.capacity.toString(amount_1.AmountUnit.ckb)} less than the min capacity ${this.occupiedCapacity().toString(amount_1.AmountUnit.ckb)}`);
                  ^

Error: cell capacity 142 less than the min capacity 315
    at Cell.spaceCheck (/home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/node_modules/@lay2/pw-core/build/main/models/cell.js:57:19)
    at new Cell (/home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/node_modules/@lay2/pw-core/build/main/models/cell.js:19:14)
    at SimpleSUDTBuilder.buildSudtCells (/home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/node_modules/@lay2/pw-core/build/main/builders/simple-sudt-builder.js:37:36)
    at SimpleSUDTBuilder.build (/home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/node_modules/@lay2/pw-core/build/main/builders/simple-sudt-builder.js:23:46)
    at PWCore.sendTransaction (/home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/node_modules/@lay2/pw-core/build/main/core.js:87:72)
    at PWCore.sendSUDT (/home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/node_modules/@lay2/pw-core/build/main/core.js:109:21)
    at file:///home/kuzi/projects/gw-gitcoin-instruction/src/examples/12-l1-to-l2-sudt-transfer/index.mjs:54:18

I think it should be possible to change receiverAmount (CKB) in buildSudtCells() method of simple-sudt-builder, because right now it's hardcoded to 142 limiting use of sendSUDT function.

Can we calculate occupiedCapacity before constructing a Cell and instead use this value?

Can not send CKB from PW to Neuron, Yokai.

  • I'm submitting a ...
    [ ] bug report

  • Summary
    I'm sending CKB from PW to another L1 CKB address and see this:
    Screenshot (182)_LI
    What does this mean and how can I solve it?

  • Other information
    I'm preparing for Yokai's IDO but now because of this, I can not do anything!
    I tried sending All ckb, 400ckb, 61ckb... but none succeeded.

Feature Request: Ability to use multiple discrete instances of PW-Core simultaneously.

  • I'm submitting a ...
    [ ] bug report
    [X] feature request
    [ ] question about the decisions made in the repository
    [ ] question about how to use this project

  • Summary
    It would be beneficial to be able to have multiple instances of PW-Core that are simultaneously initialized to different chains or with different configurations.

When building dapps that use both the mainnet and testnet, the developer is forced to continuously reinitialize PW-Core each time the focus is switched to a different network. There is no way to initialize once and store instances that can be reused as needed.

The internal use of singletons provides some simplification of structure, but this adds significant complexity to work around this limitation for any dapp that needs to work with multiple networks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.